diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..d43e71dbb900f3c2e7d903d888b5811958f5dd76
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,37 @@
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
+*.ply filter=lfs diff=lfs merge=lfs -text
+*.whl filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40227e0b96ce238a10b1609f399f7cfa4bbacdf1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+venv/
+weights/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..7713be336afef5b32dcd275266fa0044123b72fc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+---
+title: VoMP
+emoji: ๐
+colorFrom: green
+colorTo: green
+sdk: gradio
+python_version: 3.12
+sdk_version: 6.2.0
+app_file: app.py
+pinned: true
+license: apache-2.0
+short_description: Volumetric physics materials for interactive worlds
+suggested_hardware: a100-large
+suggested_storage: medium
+---
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..449f628ebd50de4eea290392e47fcdba2a619265
--- /dev/null
+++ b/app.py
@@ -0,0 +1,561 @@
+import glob
+import os
+import shutil
+import tempfile
+from typing import Dict, List, Optional, Tuple
+
+import gradio as gr
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import matplotlib.colors as mcolors
+from matplotlib.colorbar import ColorbarBase
+import numpy as np
+import spaces
+import torch
+from huggingface_hub import snapshot_download
+
+from vomp.inference import Vomp
+from vomp.inference.utils import save_materials
+
+NUM_VIEWS = 150
+PROPERTY_NAMES = ["youngs_modulus", "poissons_ratio", "density"]
+PROPERTY_DISPLAY_NAMES = {
+ "youngs_modulus": "Young's Modulus",
+ "poissons_ratio": "Poisson's Ratio",
+ "density": "Density",
+}
+
+BLENDER_LINK = (
+ "https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
+)
+BLENDER_INSTALLATION_PATH = "/tmp"
+BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
+
+EXAMPLES_DIR = "examples"
+
+model_id = "nvidia/PhysicalAI-Simulation-VoMP-Model"
+base_path = snapshot_download(repo_id=model_id, local_dir="weights")
+print(os.listdir(base_path))
+
+def _install_blender():
+ if not os.path.exists(BLENDER_PATH):
+ print("Installing Blender...")
+ os.system("sudo apt-get update")
+ os.system(
+ "sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
+ )
+ os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
+ os.system(
+ f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
+ )
+ print("Blender installed successfully!")
+
+
+def _is_gaussian_splat(file_path: str) -> bool:
+ if not file_path.lower().endswith(".ply"):
+ return False
+
+ try:
+ with open(file_path, "rb") as f:
+ header = b""
+ while True:
+ line = f.readline()
+ header += line
+ if b"end_header" in line:
+ break
+ if len(header) > 10000:
+ break
+
+ header_str = header.decode("utf-8", errors="ignore").lower()
+ gaussian_indicators = ["f_dc", "opacity", "scale_0", "rot_0"]
+ return any(indicator in header_str for indicator in gaussian_indicators)
+ except Exception:
+ return False
+
+
+def _setup_examples():
+ """Ensure examples directory exists."""
+ os.makedirs(EXAMPLES_DIR, exist_ok=True)
+
+
+_setup_examples()
+
+
+print("Loading VoMP model...")
+model = Vomp.from_checkpoint(
+ config_path="weights/inference.json",
+ geometry_checkpoint_dir="weights/geometry_transformer.pt",
+ matvae_checkpoint_dir="weights/matvae.safetensors",
+ normalization_params_path="weights/normalization_params.json",
+)
+print("VoMP model loaded successfully!")
+
+
+def _get_render_images(output_dir: str) -> List[str]:
+ renders_dir = os.path.join(output_dir, "renders")
+ if not os.path.exists(renders_dir):
+ return []
+ image_paths = sorted(glob.glob(os.path.join(renders_dir, "*.png")))
+ return image_paths
+
+
+def _create_colorbar(
+ data: np.ndarray, property_name: str, output_path: str, colormap: str = "viridis"
+) -> str:
+ fig, ax = plt.subplots(figsize=(6, 0.8))
+ fig.subplots_adjust(bottom=0.5)
+ ax.remove()
+
+ cmap = plt.cm.get_cmap(colormap)
+ norm = mcolors.Normalize(vmin=np.min(data), vmax=np.max(data))
+
+ cbar_ax = fig.add_axes([0.1, 0.4, 0.8, 0.35])
+ cb = ColorbarBase(cbar_ax, cmap=cmap, norm=norm, orientation="horizontal")
+ cb.ax.set_xlabel(
+ f"{PROPERTY_DISPLAY_NAMES.get(property_name, property_name)}", fontsize=10
+ )
+
+ plt.savefig(
+ output_path, dpi=150, bbox_inches="tight", facecolor="white", transparent=False
+ )
+ plt.close()
+ return output_path
+
+
+def _render_point_cloud_views(
+ coords: np.ndarray,
+ values: np.ndarray,
+ output_dir: str,
+ property_name: str,
+ colormap: str = "viridis",
+) -> List[str]:
+ vmin, vmax = np.min(values), np.max(values)
+ if vmax - vmin > 1e-10:
+ normalized = (values - vmin) / (vmax - vmin)
+ else:
+ normalized = np.zeros_like(values)
+
+ cmap = plt.cm.get_cmap(colormap)
+ colors = cmap(normalized)
+
+ views = [
+ (30, 45, "view1"),
+ (30, 135, "view2"),
+ (80, 45, "view3"),
+ ]
+
+ image_paths = []
+
+ for elev, azim, view_name in views:
+ fig = plt.figure(figsize=(6, 6), facecolor="#1a1a1a")
+ ax = fig.add_subplot(111, projection="3d", facecolor="#1a1a1a")
+
+ ax.scatter(
+ coords[:, 0],
+ coords[:, 1],
+ coords[:, 2],
+ c=colors,
+ s=15,
+ alpha=0.9,
+ )
+
+ ax.view_init(elev=elev, azim=azim)
+ ax.set_xlim([-0.6, 0.6])
+ ax.set_ylim([-0.6, 0.6])
+ ax.set_zlim([-0.6, 0.6])
+ ax.set_axis_off()
+ ax.set_box_aspect([1, 1, 1])
+
+ output_path = os.path.join(output_dir, f"{property_name}_{view_name}.png")
+ plt.savefig(
+ output_path,
+ dpi=150,
+ bbox_inches="tight",
+ facecolor="#1a1a1a",
+ edgecolor="none",
+ )
+ plt.close()
+
+ image_paths.append(output_path)
+
+ return image_paths
+
+
+def _create_material_visualizations(
+ material_file: str, output_dir: str
+) -> Dict[str, Tuple[List[str], str]]:
+ result = {}
+ data = np.load(material_file, allow_pickle=True)
+
+ if "voxel_data" in data:
+ voxel_data = data["voxel_data"]
+ coords = np.column_stack([voxel_data["x"], voxel_data["y"], voxel_data["z"]])
+ properties = {
+ "youngs_modulus": voxel_data["youngs_modulus"],
+ "poissons_ratio": voxel_data["poissons_ratio"],
+ "density": voxel_data["density"],
+ }
+ else:
+ if "voxel_coords_world" in data:
+ coords = data["voxel_coords_world"]
+ elif "query_coords_world" in data:
+ coords = data["query_coords_world"]
+ elif "coords" in data:
+ coords = data["coords"]
+ else:
+ print(f"Warning: No coordinate data found in {material_file}")
+ return result
+
+ properties = {}
+ property_mapping = {
+ "youngs_modulus": ["youngs_modulus", "young_modulus"],
+ "poissons_ratio": ["poissons_ratio", "poisson_ratio"],
+ "density": ["density"],
+ }
+ for prop_name, possible_names in property_mapping.items():
+ for name in possible_names:
+ if name in data:
+ properties[prop_name] = data[name]
+ break
+
+ center = (np.min(coords, axis=0) + np.max(coords, axis=0)) / 2
+ max_range = np.max(np.max(coords, axis=0) - np.min(coords, axis=0))
+ if max_range > 1e-10:
+ coords_normalized = (coords - center) / max_range
+ else:
+ coords_normalized = coords - center
+
+ for prop_name, prop_data in properties.items():
+ if prop_data is not None:
+ view_paths = _render_point_cloud_views(
+ coords_normalized, prop_data, output_dir, prop_name
+ )
+ colorbar_path = os.path.join(output_dir, f"{prop_name}_colorbar.png")
+ _create_colorbar(prop_data, prop_name, colorbar_path)
+ result[prop_name] = (view_paths, colorbar_path)
+ print(f"Created visualization for {prop_name}: {len(view_paths)} views")
+
+ return result
+
+
+@spaces.GPU()
+@torch.no_grad()
+def process_3d_model(input_file):
+ empty_result = (
+ None,
+ [],
+ None,
+ [],
+ None,
+ None,
+ [],
+ None,
+ None,
+ [],
+ None,
+ None,
+ )
+ if input_file is None:
+ return empty_result
+ output_dir = tempfile.mkdtemp(prefix="vomp_")
+ material_file = os.path.join(output_dir, "materials.npz")
+ try:
+ if _is_gaussian_splat(input_file):
+ print(f"Processing as Gaussian splat: {input_file}")
+ results = model.get_splat_materials(
+ input_file,
+ voxel_method="kaolin",
+ query_points="voxel_centers",
+ output_dir=output_dir,
+ )
+ else:
+ print(f"Processing as mesh: {input_file}")
+ _install_blender()
+ results = model.get_mesh_materials(
+ input_file,
+ blender_path=BLENDER_PATH,
+ query_points="voxel_centers",
+ output_dir=output_dir,
+ return_original_scale=True,
+ )
+
+ save_materials(results, material_file)
+ print(f"Materials saved to: {material_file}")
+
+ all_images = _get_render_images(output_dir)
+ first_image = all_images[0] if all_images else None
+
+ visualizations = _create_material_visualizations(material_file, output_dir)
+
+ youngs_views = visualizations.get("youngs_modulus", ([], None))[0]
+ youngs_colorbar = visualizations.get("youngs_modulus", ([], None))[1]
+ youngs_first = youngs_views[0] if youngs_views else None
+
+ poissons_views = visualizations.get("poissons_ratio", ([], None))[0]
+ poissons_colorbar = visualizations.get("poissons_ratio", ([], None))[1]
+ poissons_first = poissons_views[0] if poissons_views else None
+
+ density_views = visualizations.get("density", ([], None))[0]
+ density_colorbar = visualizations.get("density", ([], None))[1]
+ density_first = density_views[0] if density_views else None
+
+ return (
+ first_image,
+ all_images,
+ youngs_first,
+ youngs_views,
+ youngs_colorbar,
+ poissons_first,
+ poissons_views,
+ poissons_colorbar,
+ density_first,
+ density_views,
+ density_colorbar,
+ material_file,
+ )
+
+ except Exception as e:
+ print(f"Error processing 3D model: {e}")
+ raise gr.Error(f"Failed to process 3D model: {str(e)}")
+
+
+def update_slider_image(slider_value: int, all_images: List[str]) -> Optional[str]:
+ if not all_images or slider_value < 0 or slider_value >= len(all_images):
+ return None
+ return all_images[slider_value]
+
+
+def update_property_view(slider_value: int, views: List[str]) -> Optional[str]:
+ if not views or slider_value < 0 or slider_value >= len(views):
+ return None
+ return views[slider_value]
+
+
+css = """
+.gradio-container {
+ font-family: 'IBM Plex Sans', sans-serif;
+}
+
+.title-container {
+ text-align: center;
+ padding: 20px 0;
+}
+
+.badge-container {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 20px;
+}
+
+.badge-container a img {
+ height: 22px;
+}
+
+h1 {
+ text-align: center;
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ text-align: center;
+ color: #666;
+ font-size: 1.1rem;
+ margin-bottom: 1.5rem;
+}
+
+.input-column, .output-column {
+ min-height: 400px;
+}
+
+.output-column .row {
+ display: flex !important;
+ flex-wrap: nowrap !important;
+ gap: 16px;
+}
+
+.output-column .row > .column {
+ flex: 1 1 50% !important;
+ min-width: 0 !important;
+}
+"""
+
+title_md = """
+
+
VoMP: Predicting Volumetric Mechanical Properties
+
Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, and more.
+
+
+"""
+
+description_md = """
+Upload a Gaussian Splat (.ply) or Mesh (.obj, .glb, .stl, .gltf) to predict volumetric mechanical properties (Young's modulus, Poisson's ratio, density) for realistic physics simulation.
+"""
+
+with gr.Blocks(css=css, title="VoMP") as demo:
+ all_images_state = gr.State([])
+ youngs_views_state = gr.State([])
+ poissons_views_state = gr.State([])
+ density_views_state = gr.State([])
+
+ gr.HTML(title_md)
+ gr.Markdown(description_md)
+
+ with gr.Row():
+ # Input Column (50%)
+ with gr.Column(scale=1, elem_classes="input-column"):
+ gr.Markdown("### ๐ค Input")
+ input_model = gr.Model3D(
+ label="Upload 3D Model",
+ clear_color=[0.1, 0.1, 0.1, 1.0],
+ )
+
+ submit_btn = gr.Button(
+ "๐ Generate Materials", variant="primary", size="lg"
+ )
+
+ gr.Markdown("#### ๐ฌ Rendered Views")
+ rendered_image = gr.Image(label="Rendered View", height=250)
+
+ view_slider = gr.Slider(
+ minimum=0,
+ maximum=NUM_VIEWS - 1,
+ step=1,
+ value=0,
+ label="Browse All Views",
+ info=f"Slide to view all {NUM_VIEWS} rendered views",
+ )
+
+ # Output Column (50%)
+ with gr.Column(scale=1, elem_classes="output-column"):
+ gr.Markdown("### ๐ฅ Output - Material Properties")
+
+ # Row 1: Young's Modulus and Poisson's Ratio
+ with gr.Row():
+ with gr.Column(scale=1, min_width=200):
+ youngs_image = gr.Image(label="Young's Modulus", height=200)
+ youngs_slider = gr.Slider(
+ minimum=0,
+ maximum=2,
+ step=1,
+ value=0,
+ label="View",
+ info="Switch between 3 views",
+ )
+ youngs_colorbar = gr.Image(height=50, show_label=False)
+
+ with gr.Column(scale=1, min_width=200):
+ poissons_image = gr.Image(label="Poisson's Ratio", height=200)
+ poissons_slider = gr.Slider(
+ minimum=0,
+ maximum=2,
+ step=1,
+ value=0,
+ label="View",
+ info="Switch between 3 views",
+ )
+ poissons_colorbar = gr.Image(height=50, show_label=False)
+
+ # Row 2: Density and Download
+ with gr.Row():
+ with gr.Column(scale=1, min_width=200):
+ density_image = gr.Image(label="Density", height=200)
+ density_slider = gr.Slider(
+ minimum=0,
+ maximum=2,
+ step=1,
+ value=0,
+ label="View",
+ info="Switch between 3 views",
+ )
+ density_colorbar = gr.Image(height=50, show_label=False)
+
+ with gr.Column(scale=1, min_width=200):
+ gr.Markdown("#### ๐พ Download")
+ output_file = gr.File(
+ label="Download Materials (.npz)",
+ file_count="single",
+ )
+
+ gr.Markdown("### ๐ฏ Examples")
+ gr.Examples(
+ examples=[
+ [os.path.join(EXAMPLES_DIR, "plant.ply")],
+ [os.path.join(EXAMPLES_DIR, "dog.ply")],
+ [os.path.join(EXAMPLES_DIR, "dozer.ply")],
+ [os.path.join(EXAMPLES_DIR, "fiscus.ply")],
+ ],
+ inputs=[input_model],
+ outputs=[
+ rendered_image,
+ all_images_state,
+ youngs_image,
+ youngs_views_state,
+ youngs_colorbar,
+ poissons_image,
+ poissons_views_state,
+ poissons_colorbar,
+ density_image,
+ density_views_state,
+ density_colorbar,
+ output_file,
+ ],
+ fn=process_3d_model,
+ cache_examples=False,
+ )
+
+ # Event handlers
+ submit_btn.click(
+ fn=process_3d_model,
+ inputs=[input_model],
+ outputs=[
+ rendered_image,
+ all_images_state,
+ youngs_image,
+ youngs_views_state,
+ youngs_colorbar,
+ poissons_image,
+ poissons_views_state,
+ poissons_colorbar,
+ density_image,
+ density_views_state,
+ density_colorbar,
+ output_file,
+ ],
+ )
+
+ view_slider.change(
+ fn=update_slider_image,
+ inputs=[view_slider, all_images_state],
+ outputs=[rendered_image],
+ )
+
+ youngs_slider.change(
+ fn=update_property_view,
+ inputs=[youngs_slider, youngs_views_state],
+ outputs=[youngs_image],
+ )
+
+ poissons_slider.change(
+ fn=update_property_view,
+ inputs=[poissons_slider, poissons_views_state],
+ outputs=[poissons_image],
+ )
+
+ density_slider.change(
+ fn=update_property_view,
+ inputs=[density_slider, density_views_state],
+ outputs=[density_image],
+ )
+
+if __name__ == "__main__":
+ demo.launch()
diff --git a/deps/vomp/.gitignore b/deps/vomp/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..64d49ae3b1e5b02a3fcc4b59388f50bcc9473892
--- /dev/null
+++ b/deps/vomp/.gitignore
@@ -0,0 +1,216 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py.cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+# Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+# poetry.lock
+# poetry.toml
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
+# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
+# pdm.lock
+# pdm.toml
+.pdm-python
+.pdm-build/
+
+# pixi
+# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
+# pixi.lock
+# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
+# in the .venv directory. It is recommended not to include this directory in version control.
+.pixi
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# Redis
+*.rdb
+*.aof
+*.pid
+
+# RabbitMQ
+mnesia/
+rabbitmq/
+rabbitmq-data/
+
+# ActiveMQ
+activemq-data/
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.envrc
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+# .idea/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
+# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+# and can be added to the global gitignore or merged into this file. However, if you prefer,
+# you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
+
+# Streamlit
+.streamlit/secrets.toml
\ No newline at end of file
diff --git a/deps/vomp/ATTRIBUTIONS.md b/deps/vomp/ATTRIBUTIONS.md
new file mode 100644
index 0000000000000000000000000000000000000000..f9e5074ccf9d815029f551760e35f2a9e85ac350
--- /dev/null
+++ b/deps/vomp/ATTRIBUTIONS.md
@@ -0,0 +1,27284 @@
+# Open Source License Attribution
+
+
+VoMP uses Open Source components. You can find the details of these open-source projects along with license information below, sorted alphabetically.
+We are grateful to the developers for their contributions to open source and acknowledge these below.
+
+
+## absl-py (v2.2.2) - [Apache License 2.0](https://github.com/abseil/abseil-py)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## accelerate (v1.6.0) - [Apache License 2.0](https://github.com/huggingface/accelerate)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## addict (v2.4.0) - [MIT License](https://github.com/mewwts/addict)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2014 Mats Julian Olsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## annotated-types (v0.7.0) - [MIT License](https://github.com/annotated-types/annotated-types)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2022 the contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## anyio (v4.9.0) - [MIT License](https://anyio.readthedocs.io/en/stable/versionhistory.html)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2018 Alex Grรถnholm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## asttokens (v3.0.0) - [Apache License 2.0](https://github.com/gristlabs/asttokens)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## attrs (v25.3.0) - [MIT License](https://www.attrs.org/en/stable/changelog.html)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2015 Hynek Schlawack and the attrs contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## blinker (v1.9.0) - [MIT License](https://github.com/pallets-eco/blinker/)
+
+```
+Copyright 2010 Jason Kirtland
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## ccimport (v0.4.4) - [MIT License](https://github.com/FindDefinition/ccimport)
+
+```
+MIT License
+
+Copyright (c) 2020 FindDefinition
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## certifi (v2025.6.15) - [Mozilla Public License 2.0 (MPL 2.0)](https://github.com/certifi/python-certifi)
+
+```
+This package contains a modified version of ca-bundle.crt:
+
+ca-bundle.crt -- Bundle of CA Root Certificates
+
+This is a bundle of X.509 certificates of public Certificate Authorities
+(CA). These were automatically extracted from Mozilla's root certificates
+file (certdata.txt). This file can be found in the mozilla source tree:
+https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt
+It contains the certificates in PEM format and therefore
+can be directly used with curl / libcurl / php_curl, or with
+an Apache+mod_ssl webserver for SSL client authentication.
+Just configure this file as the SSLCACertificateFile.#
+
+***** BEGIN LICENSE BLOCK *****
+This Source Code Form is subject to the terms of the Mozilla Public License,
+v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
+one at http://mozilla.org/MPL/2.0/.
+
+***** END LICENSE BLOCK *****
+@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
+```
+
+
+## charset-normalizer (v3.4.1) - [MIT License](https://github.com/jawah/charset_normalizer/blob/master/CHANGELOG.md)
+
+```
+MIT License
+
+Copyright (c) 2025 TAHRI Ahmed R.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## click (v8.1.8) - [BSD License](https://github.com/pallets/click/)
+
+```
+Copyright 2014 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## comm (v0.2.2) - [BSD License](https://github.com/ipython/comm)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2022, Jupyter
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## ConfigArgParse (v1.7) - [MIT License](https://github.com/bw2/ConfigArgParse)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2015 bw2
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## contourpy (v1.3.1) - [BSD License](https://github.com/contourpy/contourpy)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2021-2024, ContourPy Developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## cumm-cu118 (v0.7.11) - [MIT License](https://github.com/FindDefinition/cumm)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2024 Yan Yan
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## cycler (v0.12.1) - [BSD License](https://matplotlib.org/cycler/)
+
+```
+Copyright (c) 2015, matplotlib project
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the matplotlib project nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## Cython (v3.1.2) - [Apache License 2.0](https://cython.org/)
+
+```
+The original Pyrex code as of 2006-04 is licensed under the following
+license: "Copyright stuff: Pyrex is free of restrictions. You may use,
+redistribute, modify and distribute modified versions."
+
+------------------
+
+Cython, which derives from Pyrex, is licensed under the Apache 2.0
+Software License. More precisely, all modifications and new code
+made to go from Pyrex to Cython are so licensed.
+
+See LICENSE.txt for more details.
+
+------------------
+
+The output of a Cython compilation is NOT considered a derivative
+work of Cython. Specifically, though the compilation process may
+embed snippets of varying lengths into the final output, these
+snippets, as embedded in the output, do not encumber the resulting
+output with any license restrictions.
+```
+
+
+## dash (v3.0.2) - [MIT License](https://plotly.com/dash)
+
+```
+MIT License
+
+Copyright (c) 2015-2024 Plotly Technologies Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## dataclasses-json (v0.6.7) - [MIT License](https://github.com/lidatong/dataclasses-json)
+
+```
+MIT License
+
+Copyright (c) 2019 Charles Li and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## decorator (v4.4.2) - [BSD License](https://github.com/micheles/decorator)
+
+```
+Copyright (c) 2005-2018, Michele Simionato
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ Redistributions in bytecode form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+```
+
+
+## Deprecated (v1.2.18) - [MIT License](https://github.com/laurent-laporte-pro/deprecated)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2017 Laurent LAPORTE
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## diff_gaussian_rasterization (v0.0.0) - [Gaussian-Splatting License](#)
+
+```
+Gaussian-Splatting License
+===========================
+
+**Inria** and **the Max Planck Institut for Informatik (MPII)** hold all the ownership rights on the *Software* named **gaussian-splatting**.
+The *Software* is in the process of being registered with the Agence pour la Protection des
+Programmes (APP).
+
+The *Software* is still being developed by the *Licensor*.
+
+*Licensor*'s goal is to allow the research community to use, test and evaluate
+the *Software*.
+
+## 1. Definitions
+
+*Licensee* means any person or entity that uses the *Software* and distributes
+its *Work*.
+
+*Licensor* means the owners of the *Software*, i.e Inria and MPII
+
+*Software* means the original work of authorship made available under this
+License ie gaussian-splatting.
+
+*Work* means the *Software* and any additions to or derivative works of the
+*Software* that are made available under this License.
+
+
+## 2. Purpose
+This license is intended to define the rights granted to the *Licensee* by
+Licensors under the *Software*.
+
+## 3. Rights granted
+
+For the above reasons Licensors have decided to distribute the *Software*.
+Licensors grant non-exclusive rights to use the *Software* for research purposes
+to research users (both academic and industrial), free of charge, without right
+to sublicense.. The *Software* may be used "non-commercially", i.e., for research
+and/or evaluation purposes only.
+
+Subject to the terms and conditions of this License, you are granted a
+non-exclusive, royalty-free, license to reproduce, prepare derivative works of,
+publicly display, publicly perform and distribute its *Work* and any resulting
+derivative works in any form.
+
+## 4. Limitations
+
+**4.1 Redistribution.** You may reproduce or distribute the *Work* only if (a) you do
+so under this License, (b) you include a complete copy of this License with
+your distribution, and (c) you retain without modification any copyright,
+patent, trademark, or attribution notices that are present in the *Work*.
+
+**4.2 Derivative Works.** You may specify that additional or different terms apply
+to the use, reproduction, and distribution of your derivative works of the *Work*
+("Your Terms") only if (a) Your Terms provide that the use limitation in
+Section 2 applies to your derivative works, and (b) you identify the specific
+derivative works that are subject to Your Terms. Notwithstanding Your Terms,
+this License (including the redistribution requirements in Section 3.1) will
+continue to apply to the *Work* itself.
+
+**4.3** Any other use without of prior consent of Licensors is prohibited. Research
+users explicitly acknowledge having received from Licensors all information
+allowing to appreciate the adequacy between of the *Software* and their needs and
+to undertake all necessary precautions for its execution and use.
+
+**4.4** The *Software* is provided both as a compiled library file and as source
+code. In case of using the *Software* for a publication or other results obtained
+through the use of the *Software*, users are strongly encouraged to cite the
+corresponding publications as explained in the documentation of the *Software*.
+
+## 5. Disclaimer
+
+THE USER CANNOT USE, EXPLOIT OR DISTRIBUTE THE *SOFTWARE* FOR COMMERCIAL PURPOSES
+WITHOUT PRIOR AND EXPLICIT CONSENT OF LICENSORS. YOU MUST CONTACT INRIA FOR ANY
+UNAUTHORIZED USE: stip-sophia.transfert@inria.fr . ANY SUCH ACTION WILL
+CONSTITUTE A FORGERY. THIS *SOFTWARE* IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES
+OF ANY NATURE AND ANY EXPRESS OR IMPLIED WARRANTIES, WITH REGARDS TO COMMERCIAL
+USE, PROFESSIONNAL USE, LEGAL OR NOT, OR OTHER, OR COMMERCIALISATION OR
+ADAPTATION. UNLESS EXPLICITLY PROVIDED BY LAW, IN NO EVENT, SHALL INRIA OR THE
+AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES, LOSS OF USE, DATA, OR PROFITS OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE *SOFTWARE* OR THE USE OR OTHER DEALINGS IN THE *SOFTWARE*.
+```
+
+
+## dill (v0.4.0) - [BSD License](https://github.com/uqfoundation/dill)
+
+```
+Copyright (c) 2004-2016 California Institute of Technology.
+Copyright (c) 2016-2025 The Uncertainty Quantification Foundation.
+All rights reserved.
+
+This software is available subject to the conditions and terms laid
+out below. By downloading and using this software you are agreeing
+to the following conditions.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ - Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ - Neither the names of the copyright holders nor the names of any of
+ the contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## distro (v1.9.0) - [Apache License 2.0](https://github.com/python-distro/distro)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## easydict (v1.13) - [GNU Lesser General Public License v3 (LGPLv3)](https://github.com/makinacorpus/easydict)
+
+```
+GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
+```
+
+
+## einops (v0.8.1) - [MIT License](https://github.com/arogozhnikov/einops)
+
+```
+MIT License
+
+Copyright (c) 2018 Alex Rogozhnikov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## entrypoints (v0.4) - [MIT License](https://github.com/takluyver/entrypoints)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2015 Thomas Kluyver and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## exceptiongroup (v1.2.2) - [MIT License](https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2022 Alex Grรถnholm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+This project contains code copied from the Python standard library.
+The following is the required license notice for those parts.
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation;
+All Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+```
+
+
+## executing (v2.2.0) - [MIT License](https://github.com/alexmojaki/executing)
+
+```
+MIT License
+
+Copyright (c) 2019 Alex Hall
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## fastjsonschema (v2.21.1) - [BSD License](https://github.com/horejsek/python-fastjsonschema)
+
+```
+Copyright (c) 2018, Michal Horejsek
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+ Neither the name of the {organization} nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## filelock (v3.13.1) - [Unlicense](https://github.com/tox-dev/py-filelock)
+
+```
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
+```
+
+
+## fire (v0.7.0) - [Apache License 2.0](https://github.com/google/python-fire)
+
+```
+Copyright 2017 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+
+## flash_attn (v2.7.4.post1) - [BSD License](https://github.com/Dao-AILab/flash-attention)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## Flask (v3.0.3) - [BSD License](https://github.com/pallets/flask/)
+
+```
+Copyright 2010 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## fonttools (v4.56.0) - [MIT License](http://github.com/fonttools/fonttools)
+
+```
+MIT License
+
+Copyright (c) 2017 Just van Rossum
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## fsspec (v2024.6.1) - [BSD License](https://github.com/fsspec/filesystem_spec)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2018, Martin Durant
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## git-filter-repo (v2.47.0) - [MIT License](https://github.com/newren/git-filter-repo)
+
+```
+git-filter-repo itself and most the files in this repository (exceptions
+noted below) are provided under the MIT license (see COPYING.mit).
+
+The usage of the MIT license probably makes filter-repo compatible with
+everything, but just in case, these files can also be used under whatever
+open source license[1] that git.git or libgit2 use now or in the future
+(currently GPL[2] and GPL-with-linking-exception[3]). Further, the
+examples (in contrib/filter-repo-demos/ and t/t9391/) can also be used
+under the same license that libgit2 provides their examples under (CC0,
+currently[4]).
+
+Exceptions:
+
+ - The test harness (t/test-lib.sh, t/test-lib-functions.sh) is a slightly
+ modified copy of git.git's test harness (the difference being that my
+ copy doesn't require a built version of 'git' to be present). These
+ are thus GPL2 (see COPYING.gpl), and are individually marked as such.
+
+
+[1] ...as defined by the Open Source Initiative (https://opensource.org/)
+[2] https://git.kernel.org/pub/scm/git/git.git/tree/COPYING
+[3] https://github.com/libgit2/libgit2/blob/master/COPYING
+[4] https://github.com/libgit2/libgit2/blob/master/examples/COPYING
+```
+
+
+## glcontext (v3.0.0) - [MIT License](https://github.com/moderngl/glcontext)
+
+```
+MIT License
+
+Copyright (c) 2021 ModernGL Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## grpcio (v1.71.0) - [Apache License 2.0](https://grpc.io)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-----------------------------------------------------------
+
+BSD 3-Clause License
+
+Copyright 2016, Google Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+
+-----------------------------------------------------------
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
+```
+
+
+## h11 (v0.14.0) - [MIT License](https://github.com/python-hyper/h11)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2016 Nathaniel J. Smith and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## httpcore (v1.0.7) - [BSD License](https://www.encode.io/httpcore/)
+
+```
+Copyright ยฉ 2020, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## httpx (v0.28.1) - [BSD License](https://github.com/encode/httpx)
+
+```
+Copyright ยฉ 2019, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## huggingface-hub (v0.30.1) - [Apache License 2.0](https://github.com/huggingface/huggingface_hub)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## idna (v3.10) - [BSD License](https://github.com/kjd/idna)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2013-2024, Kim Davies and contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## imageio (v2.37.0) - [BSD License](https://github.com/imageio/imageio)
+
+```
+Copyright (c) 2014-2022, imageio developers
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## imageio-ffmpeg (v0.6.0) - [BSD License](https://github.com/imageio/imageio-ffmpeg)
+
+```
+BSD 2-Clause License
+
+Copyright (c) 2019-2025, imageio
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## importlib_metadata (v8.0.0) - [Apache License 2.0](https://github.com/python/importlib_metadata)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## iniconfig (v2.1.0) - [MIT License](https://github.com/pytest-dev/iniconfig)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2010 - 2023 Holger Krekel and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## ipycanvas (v0.13.3) - [BSD License](https://github.com/jupyter-widgets-contrib/ipycanvas)
+
+```
+Copyright (c) 2019 Martin Renou
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## ipyevents (v2.0.2) - [BSD License](https://github.com/mwcraig/ipyevents)
+
+```
+Copyright (c) 2017, Matt Craig
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## ipython (v8.34.0) - [BSD License](https://ipython.org)
+
+```
+=============================
+ The IPython licensing terms
+=============================
+
+IPython is licensed under the terms of the Modified BSD License (also known as
+New or Revised or 3-Clause BSD). See the LICENSE file.
+
+
+About the IPython Development Team
+----------------------------------
+
+Fernando Perez began IPython in 2001 based on code from Janko Hauser
+ and Nathaniel Gray . Fernando is still
+the project lead.
+
+The IPython Development Team is the set of all contributors to the IPython
+project. This includes all of the IPython subprojects.
+
+The core team that coordinates development on GitHub can be found here:
+https://github.com/ipython/.
+
+Our Copyright Policy
+--------------------
+
+IPython uses a shared copyright model. Each contributor maintains copyright
+over their contributions to IPython. But, it is important to note that these
+contributions are typically only changes to the repositories. Thus, the IPython
+source code, in its entirety is not the copyright of any single person or
+institution. Instead, it is the collective copyright of the entire IPython
+Development Team. If individual contributors want to maintain a record of what
+changes/contributions they have specific copyright on, they should indicate
+their copyright in the commit message of the change, when they commit the
+change to one of the IPython repositories.
+
+With this in mind, the following banner should be used in any source code file
+to indicate the copyright and license terms:
+
+::
+
+ # Copyright (c) IPython Development Team.
+ # Distributed under the terms of the Modified BSD License.
+```
+
+
+## ipywidgets (v8.1.5) - [BSD License](http://jupyter.org)
+
+```
+Copyright (c) 2015 Project Jupyter Contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## itsdangerous (v2.2.0) - [BSD License](https://github.com/pallets/itsdangerous/)
+
+```
+Copyright 2011 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## jedi (v0.19.2) - [MIT License](https://github.com/davidhalter/jedi)
+
+```
+All contributions towards Jedi are MIT licensed.
+
+-------------------------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) <2013>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## Jinja2 (v3.1.4) - [BSD License](https://github.com/pallets/jinja/)
+
+```
+Copyright 2007 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## joblib (v1.4.2) - [BSD License](https://joblib.readthedocs.io)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2008-2021, The joblib developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## jsonschema (v4.25.1) - [MIT License](https://github.com/python-jsonschema/jsonschema)
+
+```
+Copyright (c) 2013 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## jsonschema-specifications (v2024.10.1) - [MIT License](https://github.com/python-jsonschema/jsonschema-specifications)
+
+```
+Copyright (c) 2022 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## jupyter_client (v7.4.9) - [BSD License](https://jupyter.org)
+
+```
+# Licensing terms
+
+This project is licensed under the terms of the Modified BSD License
+(also known as New or Revised or 3-Clause BSD), as follows:
+
+- Copyright (c) 2001-2015, IPython Development Team
+- Copyright (c) 2015-, Jupyter Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of the Jupyter Development Team nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+## About the Jupyter Development Team
+
+The Jupyter Development Team is the set of all contributors to the Jupyter project.
+This includes all of the Jupyter subprojects.
+
+The core team that coordinates development on GitHub can be found here:
+https://github.com/jupyter/.
+
+## Our Copyright Policy
+
+Jupyter uses a shared copyright model. Each contributor maintains copyright
+over their contributions to Jupyter. But, it is important to note that these
+contributions are typically only changes to the repositories. Thus, the Jupyter
+source code, in its entirety is not the copyright of any single person or
+institution. Instead, it is the collective copyright of the entire Jupyter
+Development Team. If individual contributors want to maintain a record of what
+changes/contributions they have specific copyright on, they should indicate
+their copyright in the commit message of the change, when they commit the
+change to one of the Jupyter repositories.
+
+With this in mind, the following banner should be used in any source code file
+to indicate the copyright and license terms:
+
+ # Copyright (c) Jupyter Development Team.
+ # Distributed under the terms of the Modified BSD License.
+```
+
+
+## jupyter_core (v5.7.2) - [BSD License](https://jupyter.org)
+
+```
+BSD 3-Clause License
+
+- Copyright (c) 2015-, Jupyter Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## jupyterlab_widgets (v3.0.13) - [BSD License](https://github.com/jupyter-widgets/ipywidgets)
+
+```
+Copyright (c) 2015 Project Jupyter Contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+
+This package bundles several JavaScript npm packages in the
+jupyterlab_widgets/static directory. Their licenses (as packaged in their
+distributions in the node_modules package installation directory) are copied
+below.
+
+------------------------------------------------------------------------------
+From css-loader/LICENSE:
+
+Copyright JS Foundation and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+------------------------------------------------------------------------------
+From style-loader/LICENSE:
+
+Copyright JS Foundation and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+------------------------------------------------------------------------------
+From backbone/backbone.js
+
+// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://backbonejs.org
+
+------------------------------------------------------------------------------
+From base-64/LICENSE
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Jameson Little
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+------------------------------------------------------------------------------
+From lodash/LICENSE
+
+Copyright OpenJS Foundation and other contributors
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
+------------------------------------------------------------------------------
+From d3-format/LICENSE:
+
+Copyright 2010-2015 Mike Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the author nor the names of contributors may be used to
+ endorse or promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+From noUISlider/LICENSE.md (https://github.com/leongersen/noUiSlider/blob/eca62f9e56aaf02f0841b36e7993adf8db3721d5/LICENSE.md)
+
+MIT License
+
+Copyright (c) 2019 Lรฉon Gersen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------------------------------------------------------------------
+From jquery/LICENSE.txt
+
+Copyright JS Foundation and other contributors, https://js.foundation/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+------------------------------------------------------------------
+From semver/LICENSE:
+
+The ISC License
+
+Copyright (c) Isaac Z. Schlueter and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+------------------------------------------------------------------
+From underscore/LICENSE
+
+Copyright (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative
+Reporters & Editors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## kaolin (v0.17.0) - [Apache License 2.0](https://github.com/NVIDIAGameWorks/kaolin)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+```
+
+
+## kiwisolver (v1.4.8) - [BSD License](https://github.com/nucleic/kiwi)
+
+```
+=========================
+ The Kiwi licensing terms
+=========================
+Kiwi is licensed under the terms of the Modified BSD License (also known as
+New or Revised BSD), as follows:
+
+Copyright (c) 2013-2024, Nucleic Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of the Nucleic Development Team nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+About Kiwi
+----------
+Chris Colbert began the Kiwi project in December 2013 in an effort to
+create a blisteringly fast UI constraint solver. Chris is still the
+project lead.
+
+The Nucleic Development Team is the set of all contributors to the Nucleic
+project and its subprojects.
+
+The core team that coordinates development on GitHub can be found here:
+http://github.com/nucleic. The current team consists of:
+
+* Chris Colbert
+
+Our Copyright Policy
+--------------------
+Nucleic uses a shared copyright model. Each contributor maintains copyright
+over their contributions to Nucleic. But, it is important to note that these
+contributions are typically only changes to the repositories. Thus, the Nucleic
+source code, in its entirety is not the copyright of any single person or
+institution. Instead, it is the collective copyright of the entire Nucleic
+Development Team. If individual contributors want to maintain a record of what
+changes/contributions they have specific copyright on, they should indicate
+their copyright in the commit message of the change, when they commit the
+change to one of the Nucleic repositories.
+
+With this in mind, the following banner should be used in any source code file
+to indicate the copyright and license terms:
+
+#------------------------------------------------------------------------------
+# Copyright (c) 2013-2024, Nucleic Development Team.
+#
+# Distributed under the terms of the Modified BSD License.
+#
+# The full license is in the file LICENSE, distributed with this software.
+#------------------------------------------------------------------------------
+```
+
+
+## lark (v1.2.2) - [MIT License](https://github.com/lark-parser/lark)
+
+```
+Copyright ยฉ 2017 Erez Shinan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## lpips (v0.1.4) - [BSD License](https://github.com/richzhang/PerceptualSimilarity)
+
+```
+Copyright (c) 2018, Richard Zhang, Phillip Isola, Alexei A. Efros, Eli Shechtman, Oliver Wang
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## Markdown (v3.7) - [BSD License](https://Python-Markdown.github.io/)
+
+```
+BSD 3-Clause License
+
+Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
+Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
+Copyright 2004 Manfred Stienstra (the original version)
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## MarkupSafe (v2.1.5) - [BSD License](https://palletsprojects.com/p/markupsafe/)
+
+```
+Copyright 2010 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## marshmallow (v3.26.1) - [MIT License](https://github.com/marshmallow-code/marshmallow)
+
+```
+Copyright Steven Loria and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## matplotlib (v3.10.1) - [Python Software Foundation License](https://matplotlib.org)
+
+```
+License agreement for matplotlib versions 1.3.0 and later
+=========================================================
+
+1. This LICENSE AGREEMENT is between the Matplotlib Development Team
+("MDT"), and the Individual or Organization ("Licensee") accessing and
+otherwise using matplotlib software in source or binary form and its
+associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, MDT
+hereby grants Licensee a nonexclusive, royalty-free, world-wide license
+to reproduce, analyze, test, perform and/or display publicly, prepare
+derivative works, distribute, and otherwise use matplotlib
+alone or in any derivative version, provided, however, that MDT's
+License Agreement and MDT's notice of copyright, i.e., "Copyright (c)
+2012- Matplotlib Development Team; All Rights Reserved" are retained in
+matplotlib alone or in any derivative version prepared by
+Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on or
+incorporates matplotlib or any part thereof, and wants to
+make the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to matplotlib .
+
+4. MDT is making matplotlib available to Licensee on an "AS
+IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB
+WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB
+ FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR
+LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING
+MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF
+THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between MDT and
+Licensee. This License Agreement does not grant permission to use MDT
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using matplotlib ,
+Licensee agrees to be bound by the terms and conditions of this License
+Agreement.
+
+License agreement for matplotlib versions prior to 1.3.0
+========================================================
+
+1. This LICENSE AGREEMENT is between John D. Hunter ("JDH"), and the
+Individual or Organization ("Licensee") accessing and otherwise using
+matplotlib software in source or binary form and its associated
+documentation.
+
+2. Subject to the terms and conditions of this License Agreement, JDH
+hereby grants Licensee a nonexclusive, royalty-free, world-wide license
+to reproduce, analyze, test, perform and/or display publicly, prepare
+derivative works, distribute, and otherwise use matplotlib
+alone or in any derivative version, provided, however, that JDH's
+License Agreement and JDH's notice of copyright, i.e., "Copyright (c)
+2002-2011 John D. Hunter; All Rights Reserved" are retained in
+matplotlib alone or in any derivative version prepared by
+Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on or
+incorporates matplotlib or any part thereof, and wants to
+make the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to matplotlib.
+
+4. JDH is making matplotlib available to Licensee on an "AS
+IS" basis. JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB
+WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB
+ FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR
+LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING
+MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF
+THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between JDH and
+Licensee. This License Agreement does not grant permission to use JDH
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using matplotlib,
+Licensee agrees to be bound by the terms and conditions of this License
+Agreement.
+```
+
+
+## matplotlib-inline (v0.1.7) - [BSD License](https://github.com/ipython/matplotlib-inline)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2019-2022, IPython Development Team.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## moderngl (v5.12.0) - [MIT License](https://github.com/moderngl/moderngl)
+
+```
+MIT License
+
+Copyright (c) 2017-2024 Szabolcs Dombi, Einar Forselv
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## mpmath (v1.3.0) - [BSD License](http://mpmath.org/)
+
+```
+Copyright (c) 2005-2021 Fredrik Johansson and mpmath contributors
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ a. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ b. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ c. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+```
+
+
+## mypy-extensions (v1.0.0) - [MIT License](https://github.com/python/mypy_extensions)
+
+```
+Mypy extensions are licensed under the terms of the MIT license, reproduced below.
+
+= = = = =
+
+The MIT License
+
+Copyright (c) 2016-2017 Jukka Lehtosalo and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+= = = = =
+```
+
+
+## narwhals (v1.33.0) - [MIT License](https://github.com/narwhals-dev/narwhals)
+
+```
+MIT License
+
+Copyright (c) 2024, Marco Gorelli
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## nbformat (v5.10.4) - [BSD License](https://jupyter.org)
+
+```
+BSD 3-Clause License
+
+- Copyright (c) 2001-2015, IPython Development Team
+- Copyright (c) 2015-, Jupyter Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## nest-asyncio (v1.6.0) - [BSD License](https://github.com/erdewit/nest_asyncio)
+
+```
+BSD 2-Clause License
+
+Copyright (c) 2018-2020, Ewald de Wit
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## networkx (v3.3) - [BSD License](https://networkx.org/)
+
+```
+NetworkX is distributed with the 3-clause BSD license.
+
+::
+
+ Copyright (C) 2004-2024, NetworkX Developers
+ Aric Hagberg
+ Dan Schult
+ Pieter Swart
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the NetworkX Developers nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## ninja (v1.11.1.4) - [Apache Software License; BSD License](http://ninja-build.org/)
+
+```
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## numpy (v1.26.4) - [BSD License](https://numpy.org)
+
+```
+Copyright (c) 2005-2023, NumPy Developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the NumPy Developers nor the names of any
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+The NumPy repository and source distributions bundle several libraries that are
+compatibly licensed. We list these here.
+
+Name: lapack-lite
+Files: numpy/linalg/lapack_lite/*
+License: BSD-3-Clause
+ For details, see numpy/linalg/lapack_lite/LICENSE.txt
+
+Name: tempita
+Files: tools/npy_tempita/*
+License: MIT
+ For details, see tools/npy_tempita/license.txt
+
+Name: dragon4
+Files: numpy/core/src/multiarray/dragon4.c
+License: MIT
+ For license text, see numpy/core/src/multiarray/dragon4.c
+
+Name: libdivide
+Files: numpy/core/include/numpy/libdivide/*
+License: Zlib
+ For license text, see numpy/core/include/numpy/libdivide/LICENSE.txt
+
+
+Note that the following files are vendored in the repository and sdist but not
+installed in built numpy packages:
+
+Name: Meson
+Files: vendored-meson/meson/*
+License: Apache 2.0
+ For license text, see vendored-meson/meson/COPYING
+
+Name: spin
+Files: .spin/cmds.py
+License: BSD-3
+ For license text, see .spin/LICENSE
+
+----
+
+This binary distribution of NumPy also bundles the following software:
+
+
+Name: OpenBLAS
+Files: numpy.libs/libopenblas*.so
+Description: bundled as a dynamically linked library
+Availability: https://github.com/OpenMathLib/OpenBLAS/
+License: BSD-3-Clause
+ Copyright (c) 2011-2014, The OpenBLAS Project
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ 3. Neither the name of the OpenBLAS project nor the names of
+ its contributors may be used to endorse or promote products
+ derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+Name: LAPACK
+Files: numpy.libs/libopenblas*.so
+Description: bundled in OpenBLAS
+Availability: https://github.com/OpenMathLib/OpenBLAS/
+License: BSD-3-Clause-Attribution
+ Copyright (c) 1992-2013 The University of Tennessee and The University
+ of Tennessee Research Foundation. All rights
+ reserved.
+ Copyright (c) 2000-2013 The University of California Berkeley. All
+ rights reserved.
+ Copyright (c) 2006-2013 The University of Colorado Denver. All rights
+ reserved.
+
+ $COPYRIGHT$
+
+ Additional copyrights may follow
+
+ $HEADER$
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ - Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer listed
+ in this license in the documentation and/or other materials
+ provided with the distribution.
+
+ - Neither the name of the copyright holders nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ The copyright holders provide no reassurances that the source code
+ provided does not infringe any patent, copyright, or any other
+ intellectual property rights of third parties. The copyright holders
+ disclaim any liability to any recipient for claims brought against
+ recipient by any third party for infringement of that parties
+ intellectual property rights.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+Name: GCC runtime library
+Files: numpy.libs/libgfortran*.so
+Description: dynamically linked to files compiled with gcc
+Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran
+License: GPL-3.0-with-GCC-exception
+ Copyright (C) 2002-2017 Free Software Foundation, Inc.
+
+ Libgfortran is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3, or (at your option)
+ any later version.
+
+ Libgfortran is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ Under Section 7 of GPL version 3, you are granted additional
+ permissions described in the GCC Runtime Library Exception, version
+ 3.1, as published by the Free Software Foundation.
+
+ You should have received a copy of the GNU General Public License and
+ a copy of the GCC Runtime Library Exception along with this program;
+ see the files COPYING3 and COPYING.RUNTIME respectively. If not, see
+ .
+
+----
+
+Full text of license texts referred to above follows (that they are
+listed below does not necessarily imply the conditions apply to the
+present binary release):
+
+----
+
+GCC RUNTIME LIBRARY EXCEPTION
+
+Version 3.1, 31 March 2009
+
+Copyright (C) 2009 Free Software Foundation, Inc.
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+This GCC Runtime Library Exception ("Exception") is an additional
+permission under section 7 of the GNU General Public License, version
+3 ("GPLv3"). It applies to a given file (the "Runtime Library") that
+bears a notice placed by the copyright holder of the file stating that
+the file is governed by GPLv3 along with this Exception.
+
+When you use GCC to compile a program, GCC may combine portions of
+certain GCC header files and runtime libraries with the compiled
+program. The purpose of this Exception is to allow compilation of
+non-GPL (including proprietary) programs to use, in this way, the
+header files and runtime libraries covered by this Exception.
+
+0. Definitions.
+
+A file is an "Independent Module" if it either requires the Runtime
+Library for execution after a Compilation Process, or makes use of an
+interface provided by the Runtime Library, but is not otherwise based
+on the Runtime Library.
+
+"GCC" means a version of the GNU Compiler Collection, with or without
+modifications, governed by version 3 (or a specified later version) of
+the GNU General Public License (GPL) with the option of using any
+subsequent versions published by the FSF.
+
+"GPL-compatible Software" is software whose conditions of propagation,
+modification and use would permit combination with GCC in accord with
+the license of GCC.
+
+"Target Code" refers to output from any compiler for a real or virtual
+target processor architecture, in executable form or suitable for
+input to an assembler, loader, linker and/or execution
+phase. Notwithstanding that, Target Code does not include data in any
+format that is used as a compiler intermediate representation, or used
+for producing a compiler intermediate representation.
+
+The "Compilation Process" transforms code entirely represented in
+non-intermediate languages designed for human-written code, and/or in
+Java Virtual Machine byte code, into Target Code. Thus, for example,
+use of source code generators and preprocessors need not be considered
+part of the Compilation Process, since the Compilation Process can be
+understood as starting with the output of the generators or
+preprocessors.
+
+A Compilation Process is "Eligible" if it is done using GCC, alone or
+with other GPL-compatible software, or if it is done without using any
+work based on GCC. For example, using non-GPL-compatible Software to
+optimize any GCC intermediate representations would not qualify as an
+Eligible Compilation Process.
+
+1. Grant of Additional Permission.
+
+You have permission to propagate a work of Target Code formed by
+combining the Runtime Library with Independent Modules, even if such
+propagation would otherwise violate the terms of GPLv3, provided that
+all Target Code was generated by Eligible Compilation Processes. You
+may then convey such a combination under terms of your choice,
+consistent with the licensing of the Independent Modules.
+
+2. No Weakening of GCC Copyleft.
+
+The availability of this Exception does not imply any general
+presumption that third-party software is unaffected by the copyleft
+requirements of the license of GCC.
+
+----
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
+
+Name: libquadmath
+Files: numpy.libs/libquadmath*.so
+Description: dynamically linked to files compiled with gcc
+Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath
+License: LGPL-2.1-or-later
+
+ GCC Quad-Precision Math Library
+ Copyright (C) 2010-2019 Free Software Foundation, Inc.
+ Written by Francois-Xavier Coudert
+
+ This file is part of the libquadmath library.
+ Libquadmath is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ Libquadmath is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+ https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+```
+
+
+## nvdiffrast (v0.3.3) - [Copyright (c) 2020, NVIDIA Corporation. All rights reserved.](https://github.com/NVlabs/nvdiffrast)
+
+```
+Copyright (c) 2020, NVIDIA Corporation. All rights reserved.
+
+
+Nvidia Source Code License (1-Way Commercial)
+
+=======================================================================
+
+1. Definitions
+
+"Licensor" means any person or entity that distributes its Work.
+
+"Software" means the original work of authorship made available under
+this License.
+
+"Work" means the Software and any additions to or derivative works of
+the Software that are made available under this License.
+
+The terms "reproduce," "reproduction," "derivative works," and
+"distribution" have the meaning as provided under U.S. copyright law;
+provided, however, that for the purposes of this License, derivative
+works shall not include works that remain separable from, or merely
+link (or bind by name) to the interfaces of, the Work.
+
+Works, including the Software, are "made available" under this License
+by including in or with the Work either (a) a copyright notice
+referencing the applicability of this License to the Work, or (b) a
+copy of this License.
+
+2. License Grants
+
+ 2.1 Copyright Grant. Subject to the terms and conditions of this
+ License, each Licensor grants to you a perpetual, worldwide,
+ non-exclusive, royalty-free, copyright license to reproduce,
+ prepare derivative works of, publicly display, publicly perform,
+ sublicense and distribute its Work and any resulting derivative
+ works in any form.
+
+3. Limitations
+
+ 3.1 Redistribution. You may reproduce or distribute the Work only
+ if (a) you do so under this License, (b) you include a complete
+ copy of this License with your distribution, and (c) you retain
+ without modification any copyright, patent, trademark, or
+ attribution notices that are present in the Work.
+
+ 3.2 Derivative Works. You may specify that additional or different
+ terms apply to the use, reproduction, and distribution of your
+ derivative works of the Work ("Your Terms") only if (a) Your Terms
+ provide that the use limitation in Section 3.3 applies to your
+ derivative works, and (b) you identify the specific derivative
+ works that are subject to Your Terms. Notwithstanding Your Terms,
+ this License (including the redistribution requirements in Section
+ 3.1) will continue to apply to the Work itself.
+
+ 3.3 Use Limitation. The Work and any derivative works thereof only
+ may be used or intended for use non-commercially. The Work or
+ derivative works thereof may be used or intended for use by Nvidia
+ or its affiliates commercially or non-commercially. As used herein,
+ "non-commercially" means for research or evaluation purposes only
+ and not for any direct or indirect monetary gain.
+
+ 3.4 Patent Claims. If you bring or threaten to bring a patent claim
+ against any Licensor (including any claim, cross-claim or
+ counterclaim in a lawsuit) to enforce any patents that you allege
+ are infringed by any Work, then your rights under this License from
+ such Licensor (including the grant in Section 2.1) will terminate
+ immediately.
+
+ 3.5 Trademarks. This License does not grant any rights to use any
+ Licensor's or its affiliates' names, logos, or trademarks, except
+ as necessary to reproduce the notices described in this License.
+
+ 3.6 Termination. If you violate any term of this License, then your
+ rights under this License (including the grant in Section 2.1) will
+ terminate immediately.
+
+4. Disclaimer of Warranty.
+
+THE WORK IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR
+NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER
+THIS LICENSE.
+
+5. Limitation of Liability.
+
+EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL
+THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE
+SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT,
+INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF
+OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK
+(INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION,
+LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER
+COMMERCIAL DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGES.
+
+=======================================================================
+```
+
+
+## open3d (v0.19.0) - [MIT License](https://www.open3d.org)
+
+```
+The MIT License (MIT)
+
+Open3D: www.open3d.org
+Copyright (c) 2018-2023 www.open3d.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## openai (v2.8.1) - [Apache License 2.0](https://github.com/openai/openai-python)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2025 OpenAI
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## opencv-python-headless (v4.10.0.84) - [Apache License 2.0](https://github.com/opencv/opencv-python)
+
+```
+OpenCV library is redistributed within opencv-python package.
+This license applies to OpenCV binary in the directory cv2/.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+------------------------------------------------------------------------------
+libvpx is redistributed within all opencv-python Linux packages.
+This license applies to libvpx binary in the directory cv2/.
+
+Copyright (c) 2010, The WebM Project authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google, nor the WebM Project, nor the names
+ of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+FFmpeg is redistributed within all opencv-python packages.
+
+Libbluray, libgnutls, libnettle, libhogweed, libintl, libmp3lame, libp11,
+librtmp, libsoxr and libtasn1 are redistributed within all opencv-python macOS packages.
+
+This license applies to the above library binaries in the directory cv2/.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+------------------------------------------------------------------------------
+Qt 5 is redistributed within non-headless opencv-python Linux and macOS packages.
+libgmp is redistributed within opencv-python macOS packages.
+libidn2 is redistributed within opencv-python macOS packages.
+libunistring is redistributed within opencv-python macOS packages.
+This license applies to the above binaries in the directory cv2/.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
+
+------------------------------------------------------------------------------
+bzip2 is redistributed within all opencv-python Linux packages.
+This license applies to libbz2 binary in the directory cv2/.
+
+This program, "bzip2", the associated library "libbzip2", and all
+documentation, are copyright (C) 1996-2010 Julian R Seward. All
+rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Julian Seward, jseward@bzip.org
+bzip2/libbzip2 version 1.0.6 of 6 September 2010
+
+------------------------------------------------------------------------------
+libcrypto and libssl are redistributed within all opencv-python Linux and macOS packages.
+libopencore-amrnb and libopencore-amrwb are redistributed within all opencv-python Linux and macOS packages.
+This license applies to above binaries in the directory cv2/.
+
+ LICENSE ISSUES
+ ==============
+
+ The OpenSSL toolkit stays under a double license, i.e. both the conditions of
+ the OpenSSL License and the original SSLeay license apply to the toolkit.
+ See below for the actual license texts.
+
+ OpenSSL License
+ ---------------
+
+/* ====================================================================
+ * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this
+ * software must display the following acknowledgment:
+ * "This product includes software developed by the OpenSSL Project
+ * for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+ *
+ * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For written permission, please contact
+ * openssl-core@openssl.org.
+ *
+ * 5. Products derived from this software may not be called "OpenSSL"
+ * nor may "OpenSSL" appear in their names without prior written
+ * permission of the OpenSSL Project.
+ *
+ * 6. Redistributions of any form whatsoever must retain the following
+ * acknowledgment:
+ * "This product includes software developed by the OpenSSL Project
+ * for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+ * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This product includes cryptographic software written by Eric Young
+ * (eay@cryptsoft.com). This product includes software written by Tim
+ * Hudson (tjh@cryptsoft.com).
+ *
+ */
+
+ Original SSLeay License
+ -----------------------
+
+/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
+ * All rights reserved.
+ *
+ * This package is an SSL implementation written
+ * by Eric Young (eay@cryptsoft.com).
+ * The implementation was written so as to conform with Netscapes SSL.
+ *
+ * This library is free for commercial and non-commercial use as long as
+ * the following conditions are adhered to. The following conditions
+ * apply to all code found in this distribution, be it the RC4, RSA,
+ * lhash, DES, etc., code; not just the SSL code. The SSL documentation
+ * included with this distribution is covered by the same copyright terms
+ * except that the holder is Tim Hudson (tjh@cryptsoft.com).
+ *
+ * Copyright remains Eric Young's, and as such any Copyright notices in
+ * the code are not to be removed.
+ * If this package is used in a product, Eric Young should be given attribution
+ * as the author of the parts of the library used.
+ * This can be in the form of a textual message at program startup or
+ * in documentation (online or textual) provided with the package.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ * must display the following acknowledgement:
+ * "This product includes cryptographic software written by
+ * Eric Young (eay@cryptsoft.com)"
+ * The word 'cryptographic' can be left out if the routines from the library
+ * being used are not cryptographic related :-).
+ * 4. If you include any Windows specific code (or a derivative thereof) from
+ * the apps directory (application code) you must include an acknowledgement:
+ * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+ *
+ * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * The licence and distribution terms for any publicly available version or
+ * derivative of this code cannot be changed. i.e. this code cannot simply be
+ * copied and put under another distribution licence
+ * [including the GNU Public Licence.]
+ */
+
+------------------------------------------------------------------------------
+libfontconfig is redistributed within all opencv-python macOS packages.
+This license applies to libfontconfig binary in the directory cv2/.
+
+Copyright ยฉ 2000,2001,2002,2003,2004,2006,2007 Keith Packard
+Copyright ยฉ 2005 Patrick Lam
+Copyright ยฉ 2009 Roozbeh Pournader
+Copyright ยฉ 2008,2009 Red Hat, Inc.
+Copyright ยฉ 2008 Danilo ล egan
+Copyright ยฉ 2012 Google, Inc.
+
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation, and that the name of the author(s) not be used in
+advertising or publicity pertaining to distribution of the software without
+specific, written prior permission. The authors make no
+representations about the suitability of this software for any purpose. It
+is provided "as is" without express or implied warranty.
+
+THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
+EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+------------------------------------------------------------------------------
+libfreetype is redistributed within opencv-python Linux and macOS packages.
+This license applies to libfreetype binary in the directory cv2/.
+
+ The FreeType Project LICENSE
+ ----------------------------
+
+ 2006-Jan-27
+
+ Copyright 1996-2002, 2006 by
+ David Turner, Robert Wilhelm, and Werner Lemberg
+
+
+
+Introduction
+============
+
+ The FreeType Project is distributed in several archive packages;
+ some of them may contain, in addition to the FreeType font engine,
+ various tools and contributions which rely on, or relate to, the
+ FreeType Project.
+
+ This license applies to all files found in such packages, and
+ which do not fall under their own explicit license. The license
+ affects thus the FreeType font engine, the test programs,
+ documentation and makefiles, at the very least.
+
+ This license was inspired by the BSD, Artistic, and IJG
+ (Independent JPEG Group) licenses, which all encourage inclusion
+ and use of free software in commercial and freeware products
+ alike. As a consequence, its main points are that:
+
+ o We don't promise that this software works. However, we will be
+ interested in any kind of bug reports. (`as is' distribution)
+
+ o You can use this software for whatever you want, in parts or
+ full form, without having to pay us. (`royalty-free' usage)
+
+ o You may not pretend that you wrote this software. If you use
+ it, or only parts of it, in a program, you must acknowledge
+ somewhere in your documentation that you have used the
+ FreeType code. (`credits')
+
+ We specifically permit and encourage the inclusion of this
+ software, with or without modifications, in commercial products.
+ We disclaim all warranties covering The FreeType Project and
+ assume no liability related to The FreeType Project.
+
+
+ Finally, many people asked us for a preferred form for a
+ credit/disclaimer to use in compliance with this license. We thus
+ encourage you to use the following text:
+
+ """
+ Portions of this software are copyright ยฉ The FreeType
+ Project (www.freetype.org). All rights reserved.
+ """
+
+ Please replace with the value from the FreeType version you
+ actually use.
+
+
+Legal Terms
+===========
+
+0. Definitions
+--------------
+
+ Throughout this license, the terms `package', `FreeType Project',
+ and `FreeType archive' refer to the set of files originally
+ distributed by the authors (David Turner, Robert Wilhelm, and
+ Werner Lemberg) as the `FreeType Project', be they named as alpha,
+ beta or final release.
+
+ `You' refers to the licensee, or person using the project, where
+ `using' is a generic term including compiling the project's source
+ code as well as linking it to form a `program' or `executable'.
+ This program is referred to as `a program using the FreeType
+ engine'.
+
+ This license applies to all files distributed in the original
+ FreeType Project, including all source code, binaries and
+ documentation, unless otherwise stated in the file in its
+ original, unmodified form as distributed in the original archive.
+ If you are unsure whether or not a particular file is covered by
+ this license, you must contact us to verify this.
+
+ The FreeType Project is copyright (C) 1996-2000 by David Turner,
+ Robert Wilhelm, and Werner Lemberg. All rights reserved except as
+ specified below.
+
+1. No Warranty
+--------------
+
+ THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
+ USE, OF THE FREETYPE PROJECT.
+
+2. Redistribution
+-----------------
+
+ This license grants a worldwide, royalty-free, perpetual and
+ irrevocable right and license to use, execute, perform, compile,
+ display, copy, create derivative works of, distribute and
+ sublicense the FreeType Project (in both source and object code
+ forms) and derivative works thereof for any purpose; and to
+ authorize others to exercise some or all of the rights granted
+ herein, subject to the following conditions:
+
+ o Redistribution of source code must retain this license file
+ (`FTL.TXT') unaltered; any additions, deletions or changes to
+ the original files must be clearly indicated in accompanying
+ documentation. The copyright notices of the unaltered,
+ original files must be preserved in all copies of source
+ files.
+
+ o Redistribution in binary form must provide a disclaimer that
+ states that the software is based in part of the work of the
+ FreeType Team, in the distribution documentation. We also
+ encourage you to put an URL to the FreeType web page in your
+ documentation, though this isn't mandatory.
+
+ These conditions apply to any software derived from or based on
+ the FreeType Project, not just the unmodified files. If you use
+ our work, you must acknowledge us. However, no fee need be paid
+ to us.
+
+3. Advertising
+--------------
+
+ Neither the FreeType authors and contributors nor you shall use
+ the name of the other for commercial, advertising, or promotional
+ purposes without specific prior written permission.
+
+ We suggest, but do not require, that you use one or more of the
+ following phrases to refer to this software in your documentation
+ or advertising materials: `FreeType Project', `FreeType Engine',
+ `FreeType library', or `FreeType Distribution'.
+
+ As you have not signed this license, you are not required to
+ accept it. However, as the FreeType Project is copyrighted
+ material, only this license, or another one contracted with the
+ authors, grants you the right to use, distribute, and modify it.
+ Therefore, by using, distributing, or modifying the FreeType
+ Project, you indicate that you understand and accept all the terms
+ of this license.
+
+4. Contacts
+-----------
+
+ There are two mailing lists related to FreeType:
+
+ o freetype@nongnu.org
+
+ Discusses general use and applications of FreeType, as well as
+ future and wanted additions to the library and distribution.
+ If you are looking for support, start in this list if you
+ haven't found anything to help you in the documentation.
+
+ o freetype-devel@nongnu.org
+
+ Discusses bugs, as well as engine internals, design issues,
+ specific licenses, porting, etc.
+
+ Our home page can be found at
+
+ https://www.freetype.org
+
+------------------------------------------------------------------------------
+libpng is redistributed within all opencv-python Linux and macOS packages.
+This license applies to libpng binary in the directory cv2/.
+
+PNG Reference Library License version 2
+---------------------------------------
+
+ * Copyright (c) 1995-2019 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2019 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement. In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+ must not claim that you wrote the original software. If you
+ use this software in a product, an acknowledgment in the product
+ documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+
+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
+-----------------------------------------------------------------------
+
+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
+derived from libpng-1.0.6, and are distributed according to the same
+disclaimer and license as libpng-1.0.6 with the following individuals
+added to the list of Contributing Authors:
+
+ Simon-Pierre Cadieux
+ Eric S. Raymond
+ Mans Rullgard
+ Cosmin Truta
+ Gilles Vollant
+ James Yu
+ Mandar Sahastrabuddhe
+ Google Inc.
+ Vadim Barkov
+
+and with the following additions to the disclaimer:
+
+ There is no warranty against interference with your enjoyment of
+ the library or against infringement. There is no warranty that our
+ efforts or the library will fulfill any of your particular purposes
+ or needs. This library is provided with all faults, and the entire
+ risk of satisfactory quality, performance, accuracy, and effort is
+ with the user.
+
+Some files in the "contrib" directory and some configure-generated
+files that are distributed with libpng have other copyright owners, and
+are released under other open source licenses.
+
+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
+libpng-0.96, and are distributed according to the same disclaimer and
+license as libpng-0.96, with the following individuals added to the
+list of Contributing Authors:
+
+ Tom Lane
+ Glenn Randers-Pehrson
+ Willem van Schaik
+
+libpng versions 0.89, June 1996, through 0.96, May 1997, are
+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
+and are distributed according to the same disclaimer and license as
+libpng-0.88, with the following individuals added to the list of
+Contributing Authors:
+
+ John Bowler
+ Kevin Bracey
+ Sam Bushell
+ Magnus Holmgren
+ Greg Roelofs
+ Tom Tanner
+
+Some files in the "scripts" directory have other copyright owners,
+but are released under this license.
+
+libpng versions 0.5, May 1995, through 0.88, January 1996, are
+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+For the purposes of this copyright and license, "Contributing Authors"
+is defined as the following set of individuals:
+
+ Andreas Dilger
+ Dave Martindale
+ Guy Eric Schalnat
+ Paul Schmidt
+ Tim Wegner
+
+The PNG Reference Library is supplied "AS IS". The Contributing
+Authors and Group 42, Inc. disclaim all warranties, expressed or
+implied, including, without limitation, the warranties of
+merchantability and of fitness for any purpose. The Contributing
+Authors and Group 42, Inc. assume no liability for direct, indirect,
+incidental, special, exemplary, or consequential damages, which may
+result from the use of the PNG Reference Library, even if advised of
+the possibility of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+source code, or portions hereof, for any purpose, without fee, subject
+to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented.
+
+ 2. Altered versions must be plainly marked as such and must not
+ be misrepresented as being the original source.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+The Contributing Authors and Group 42, Inc. specifically permit,
+without fee, and encourage the use of this source code as a component
+to supporting the PNG file format in commercial products. If you use
+this source code in a product, acknowledgment is not required but would
+be appreciated.
+
+------------------------------------------------------------------------------
+libz is redistributed within all opencv-python Linux packages.
+This license applies to libz binary in the directory cv2/.
+
+ Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ Jean-loup Gailly Mark Adler
+ jloup@gzip.org madler@alumni.caltech.edu
+
+------------------------------------------------------------------------------
+libdav1d is redistributed within opencv-python macOS packages.
+This license applies to libdav1d binary in the directory cv2/.
+
+Copyright ยฉ 2018-2019, VideoLAN and dav1d authors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libffi is redistributed within opencv-python macOS packages.
+This license applies to libffi binary in the directory cv2/.
+
+libffi - Copyright (c) 1996-2020 Anthony Green, Red Hat, Inc and others.
+See source files for details.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+``Software''), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+------------------------------------------------------------------------------
+libogg is redistributed within opencv-python macOS packages.
+This license applies to libogg binary in the directory cv2/.
+
+Copyright (c) 2002, Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libopenjp2 is redistributed within opencv-python macOS packages.
+This license applies to libopenjp2 binary in the directory cv2/.
+
+The copyright in this software is being made available under the 2-clauses
+BSD License, included below. This software may be subject to other third
+party and contributor rights, including patent rights, and no such rights
+are granted under this license.
+
+Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium
+Copyright (c) 2002-2014, Professor Benoit Macq
+Copyright (c) 2003-2014, Antonin Descampe
+Copyright (c) 2003-2009, Francois-Olivier Devaux
+Copyright (c) 2005, Herve Drolon, FreeImage Team
+Copyright (c) 2002-2003, Yannick Verschueren
+Copyright (c) 2001-2003, David Janssens
+Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France
+Copyright (c) 2012, CS Systemes d'Information, France
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libopus is redistributed within opencv-python macOS packages.
+This license applies to libopus binary in the directory cv2/.
+
+Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
+ Jean-Marc Valin, Timothy B. Terriberry,
+ CSIRO, Gregory Maxwell, Mark Borgerding,
+ Erik de Castro Lopo
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of Internet Society, IETF or IETF Trust, nor the
+names of specific contributors, may be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Opus is subject to the royalty-free patent licenses which are
+specified at:
+
+Xiph.Org Foundation:
+https://datatracker.ietf.org/ipr/1524/
+
+Microsoft Corporation:
+https://datatracker.ietf.org/ipr/1914/
+
+Broadcom Corporation:
+https://datatracker.ietf.org/ipr/1526/
+
+------------------------------------------------------------------------------
+librav1e is redistributed within opencv-python macOS packages.
+This license applies to librav1e binary in the directory cv2/.
+
+BSD 2-Clause License
+
+Copyright (c) 2017-2020, the rav1e contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libsnappy is redistributed within opencv-python macOS packages.
+This license applies to libsnappy binary in the directory cv2/.
+
+Copyright 2011, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libspeex is redistributed within opencv-python macOS packages.
+This license applies to libspeex binary in the directory cv2/.
+
+Copyright 2002-2008 Xiph.org Foundation
+Copyright 2002-2008 Jean-Marc Valin
+Copyright 2005-2007 Analog Devices Inc.
+Copyright 2005-2008 Commonwealth Scientific and Industrial Research
+ Organisation (CSIRO)
+Copyright 1993, 2002, 2006 David Rowe
+Copyright 2003 EpicGames
+Copyright 1992-1994 Jutta Degener, Carsten Bormann
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libsrt is redistributed within opencv-python macOS packages.
+This license applies to libsrt binary in the directory cv2/.
+
+/*
+ *
+ * Copyright (c) 2001-2017 Cisco Systems, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * Neither the name of the Cisco Systems, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+
+ Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
+
+------------------------------------------------------------------------------
+libtheoradec and libtheoraenc are redistributed within opencv-python macOS packages.
+This license applies to libtheoradec and libtheoraenc binaries in the directory cv2/.
+
+ Copyright (C) 2002-2009 Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libwebp and libwebpmux are redistributed within all opencv-python packages.
+This license applies to libwebp and libwebpmux binaries in the directory cv2/.
+
+Copyright (c) 2010, Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+libvorbis and libvorbisenc are redistributed within opencv-python macOS packages.
+This license applies to libvorbis and libvorbisenc binaries in the directory cv2/.
+
+Copyright (c) 2002-2020 Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+Libxcb utility libraries are redistributed within opencv-python non-headless Linux packages.
+This license applies to libxcb related binaries in the directory cv2/.
+
+Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett.
+All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall
+be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors
+or their institutions shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this
+Software without prior written authorization from the
+authors.
+
+------------------------------------------------------------------------------
+Libxcb-image is redistributed within opencv-python non-headless Linux packages.
+This license applies to libxcb-image binary in the directory cv2/.
+
+Copyright ยฉ 2007-2008 Bart Massey
+Copyright ยฉ 2008 Julien Danjou
+Copyright ยฉ 2008 Keith Packard
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors or
+their institutions shall not be used in advertising or otherwise to
+promote the sale, use or other dealings in this Software without
+prior written authorization from the authors.
+
+------------------------------------------------------------------------------
+Libxcb-util is redistributed within opencv-python non-headless Linux packages.
+This license applies to libxcb-util binary in the directory cv2/.
+
+Copyright ยฉ 2008 Bart Massey
+Copyright ยฉ 2008 Ian Osgood
+Copyright ยฉ 2008 Jamey Sharp
+Copyright ยฉ 2008 Josh Triplett
+Copyright ยฉ 2008-2009 Julien Danjou
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors or
+their institutions shall not be used in advertising or otherwise to
+promote the sale, use or other dealings in this Software without
+prior written authorization from the authors.
+
+------------------------------------------------------------------------------
+Libxcb-render-util is redistributed within opencv-python non-headless Linux packages.
+This license applies to libxcb-render-util binary in the directory cv2/.
+
+Copyright ยฉ 2000 Keith Packard
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation, and that the name of Keith Packard not be used in
+advertising or publicity pertaining to distribution of the software without
+specific, written prior permission. Keith Packard makes no
+representations about the suitability of this software for any purpose. It
+is provided "as is" without express or implied warranty.
+
+KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
+EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+Copyright ยฉ 2006 Jamey Sharp.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors or their
+institutions shall not be used in advertising or otherwise to promote the
+sale, use or other dealings in this Software without prior written
+authorization from the authors.
+
+Copyright ยฉ 2006 Ian Osgood
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors or their
+institutions shall not be used in advertising or otherwise to promote the
+sale, use or other dealings in this Software without prior written
+authorization from the authors.
+
+------------------------------------------------------------------------------
+Libxcb-icccm is redistributed within opencv-python non-headless Linux packages.
+This license applies to Libxcb-icccm binary in the directory cv2/.
+
+Copyright ยฉ 2008-2011 Arnaud Fontaine
+Copyright ยฉ 2007-2008 Vincent Torri
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors or
+their institutions shall not be used in advertising or otherwise to
+promote the sale, use or other dealings in this Software without
+prior written authorization from the authors.
+
+------------------------------------------------------------------------------
+libXau is redistributed within opencv-python non-headless Linux packages.
+This license applies to libXau binary in the directory cv2/.
+
+Copyright 1988, 1993, 1994, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
+
+------------------------------------------------------------------------------
+Vulkan headers are redistributed within all opencv-python packages.
+This license applies to Vulkan headers in the directory 3rdparty/include/vulkan.
+
+Copyright (c) 2015-2018 The Khronos Group Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+------------------------------------------------------------------------------
+Libjpeg-turbo is redistributed within all opencv-python packages as build option.
+
+libjpeg-turbo Licenses
+======================
+
+libjpeg-turbo is covered by three compatible BSD-style open source licenses:
+
+- The IJG (Independent JPEG Group) License, which is listed in
+ [README.ijg](README.ijg)
+
+ This license applies to the libjpeg API library and associated programs
+ (any code inherited from libjpeg, and any modifications to that code.)
+
+- The Modified (3-clause) BSD License, which is listed below
+
+ This license covers the TurboJPEG API library and associated programs, as
+ well as the build system.
+
+- The [zlib License](https://opensource.org/licenses/Zlib)
+
+ This license is a subset of the other two, and it covers the libjpeg-turbo
+ SIMD extensions.
+
+
+Complying with the libjpeg-turbo Licenses
+=========================================
+
+This section provides a roll-up of the libjpeg-turbo licensing terms, to the
+best of our understanding.
+
+1. If you are distributing a modified version of the libjpeg-turbo source,
+ then:
+
+ 1. You cannot alter or remove any existing copyright or license notices
+ from the source.
+
+ **Origin**
+ - Clause 1 of the IJG License
+ - Clause 1 of the Modified BSD License
+ - Clauses 1 and 3 of the zlib License
+
+ 2. You must add your own copyright notice to the header of each source
+ file you modified, so others can tell that you modified that file (if
+ there is not an existing copyright header in that file, then you can
+ simply add a notice stating that you modified the file.)
+
+ **Origin**
+ - Clause 1 of the IJG License
+ - Clause 2 of the zlib License
+
+ 3. You must include the IJG README file, and you must not alter any of the
+ copyright or license text in that file.
+
+ **Origin**
+ - Clause 1 of the IJG License
+
+2. If you are distributing only libjpeg-turbo binaries without the source, or
+ if you are distributing an application that statically links with
+ libjpeg-turbo, then:
+
+ 1. Your product documentation must include a message stating:
+
+ This software is based in part on the work of the Independent JPEG
+ Group.
+
+ **Origin**
+ - Clause 2 of the IJG license
+
+ 2. If your binary distribution includes or uses the TurboJPEG API, then
+ your product documentation must include the text of the Modified BSD
+ License (see below.)
+
+ **Origin**
+ - Clause 2 of the Modified BSD License
+
+3. You cannot use the name of the IJG or The libjpeg-turbo Project or the
+ contributors thereof in advertising, publicity, etc.
+
+ **Origin**
+ - IJG License
+ - Clause 3 of the Modified BSD License
+
+4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be
+ free of defects, nor do we accept any liability for undesirable
+ consequences resulting from your use of the software.
+
+ **Origin**
+ - IJG License
+ - Modified BSD License
+ - zlib License
+
+
+The Modified (3-clause) BSD License
+===================================
+
+Copyright (C)2009-2022 D. R. Commander. All Rights Reserved.
+Copyright (C)2015 Viktor Szathmรกry. All Rights Reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+- Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+- Neither the name of the libjpeg-turbo Project nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+
+Why Three Licenses?
+===================
+
+The zlib License could have been used instead of the Modified (3-clause) BSD
+License, and since the IJG License effectively subsumes the distribution
+conditions of the zlib License, this would have effectively placed
+libjpeg-turbo binary distributions under the IJG License. However, the IJG
+License specifically refers to the Independent JPEG Group and does not extend
+attribution and endorsement protections to other entities. Thus, it was
+desirable to choose a license that granted us the same protections for new code
+that were granted to the IJG for code derived from their software.
+
+------------------------------------------------------------------------------
+Libspng is redistributed within all opencv-python packages as build option.
+
+BSD 2-Clause License
+
+Copyright (c) 2018-2022, Randy
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+QUIRC library is redistributed within all opencv-python packages.
+
+quirc -- QR-code recognition library
+Copyright (C) 2010-2012 Daniel Beer
+
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted, provided that the
+above copyright notice and this permission notice appear in all
+copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+------------------------------------------------------------------------------
+Flatbuffers library is redistributed within all opencv-python packages.
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+------------------------------------------------------------------------------
+Protobuf library is redistributed within all opencv-python packages.
+
+Copyright 2008 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it. This code is not
+standalone and requires a support library to be linked with it. This
+support library is itself covered by the above license.
+
+------------------------------------------------------------------------------
+OpenJPEG library is redistributed within all opencv-python packages.
+
+/*
+ * The copyright in this software is being made available under the 2-clauses
+ * BSD License, included below. This software may be subject to other third
+ * party and contributor rights, including patent rights, and no such rights
+ * are granted under this license.
+ *
+ * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium
+ * Copyright (c) 2002-2014, Professor Benoit Macq
+ * Copyright (c) 2003-2014, Antonin Descampe
+ * Copyright (c) 2003-2009, Francois-Olivier Devaux
+ * Copyright (c) 2005, Herve Drolon, FreeImage Team
+ * Copyright (c) 2002-2003, Yannick Verschueren
+ * Copyright (c) 2001-2003, David Janssens
+ * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France
+ * Copyright (c) 2012, CS Systemes d'Information, France
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+------------------------------------------------------------------------------
+TIFF library is redistributed within all opencv-python packages.
+
+Copyright (c) 1988-1997 Sam Leffler
+Copyright (c) 1991-1997 Silicon Graphics, Inc.
+
+Permission to use, copy, modify, distribute, and sell this software and
+its documentation for any purpose is hereby granted without fee, provided
+that (i) the above copyright notices and this permission notice appear in
+all copies of the software and related documentation, and (ii) the names of
+Sam Leffler and Silicon Graphics may not be used in any advertising or
+publicity relating to the software without the specific, prior written
+permission of Sam Leffler and Silicon Graphics.
+
+THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
+
+IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
+ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
+LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+OF THIS SOFTWARE.
+
+------------------------------------------------------------------------------
+OpenEXR library is redistributed within all opencv-python packages.
+
+Copyright (c) 2006, Industrial Light & Magic, a division of Lucasfilm
+Entertainment Company Ltd. Portions contributed and copyright held by
+others as indicated. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the following
+ disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of Industrial Light & Magic nor the names of
+ any other contributors to this software may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## packaging (v24.2) - [Apache Software License; BSD License](https://github.com/pypa/packaging)
+
+```
+This software is made available under the terms of *either* of the licenses
+found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
+under the terms of *both* these licenses.
+```
+
+
+## pandas (v2.2.3) - [BSD License](https://pandas.pydata.org)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
+All rights reserved.
+
+Copyright (c) 2011-2023, Open source contributors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+Copyright (c) 2010-2019 Keith Goodman
+Copyright (c) 2019 Bottleneck Developers
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.Copyright 2017- Paul Ganssle
+Copyright 2017- dateutil contributors (see AUTHORS file)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+The above license applies to all contributions after 2017-12-01, as well as
+all contributions that have been re-licensed (see AUTHORS file for the list of
+contributors who have re-licensed their code).
+--------------------------------------------------------------------------------
+dateutil - Extensions to the standard Python datetime module.
+
+Copyright (c) 2003-2011 - Gustavo Niemeyer
+Copyright (c) 2012-2014 - Tomi Pievilรคinen
+Copyright (c) 2014-2016 - Yaron de Leeuw
+Copyright (c) 2015- - Paul Ganssle
+Copyright (c) 2015- - dateutil contributors (see AUTHORS file)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The above BSD License Applies to all code, even that also covered by Apache 2.0.# MIT License
+
+Copyright (c) 2019 Hadley Wickham; RStudio; and Evan Miller
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+Based on http://opensource.org/licenses/MIT
+
+This is a template. Complete and ship as file LICENSE the following 2
+lines (only)
+
+YEAR:
+COPYRIGHT HOLDER:
+
+and specify as
+
+License: MIT + file LICENSE
+
+Copyright (c) ,
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+The MIT License
+
+Copyright (c) 2008- Attractive Chaos
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.musl as a whole is licensed under the following standard MIT license:
+
+----------------------------------------------------------------------
+Copyright ยฉ 2005-2020 Rich Felker, et al.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+----------------------------------------------------------------------
+
+Authors/contributors include:
+
+A. Wilcox
+Ada Worcester
+Alex Dowad
+Alex Suykov
+Alexander Monakov
+Andre McCurdy
+Andrew Kelley
+Anthony G. Basile
+Aric Belsito
+Arvid Picciani
+Bartosz Brachaczek
+Benjamin Peterson
+Bobby Bingham
+Boris Brezillon
+Brent Cook
+Chris Spiegel
+Clรฉment Vasseur
+Daniel Micay
+Daniel Sabogal
+Daurnimator
+David Carlier
+David Edelsohn
+Denys Vlasenko
+Dmitry Ivanov
+Dmitry V. Levin
+Drew DeVault
+Emil Renner Berthing
+Fangrui Song
+Felix Fietkau
+Felix Janda
+Gianluca Anzolin
+Hauke Mehrtens
+He X
+Hiltjo Posthuma
+Isaac Dunham
+Jaydeep Patil
+Jens Gustedt
+Jeremy Huntwork
+Jo-Philipp Wich
+Joakim Sindholt
+John Spencer
+Julien Ramseier
+Justin Cormack
+Kaarle Ritvanen
+Khem Raj
+Kylie McClain
+Leah Neukirchen
+Luca Barbato
+Luka Perkov
+M Farkas-Dyck (Strake)
+Mahesh Bodapati
+Markus Wichmann
+Masanori Ogino
+Michael Clark
+Michael Forney
+Mikhail Kremnyov
+Natanael Copa
+Nicholas J. Kain
+orc
+Pascal Cuoq
+Patrick Oppenlander
+Petr Hosek
+Petr Skocik
+Pierre Carrier
+Reini Urban
+Rich Felker
+Richard Pennington
+Ryan Fairfax
+Samuel Holland
+Segev Finer
+Shiz
+sin
+Solar Designer
+Stefan Kristiansson
+Stefan O'Rear
+Szabolcs Nagy
+Timo Terรคs
+Trutz Behn
+Valentin Ochs
+Will Dietz
+William Haddon
+William Pitcock
+
+Portions of this software are derived from third-party works licensed
+under terms compatible with the above MIT license:
+
+The TRE regular expression implementation (src/regex/reg* and
+src/regex/tre*) is Copyright ยฉ 2001-2008 Ville Laurikari and licensed
+under a 2-clause BSD license (license text in the source files). The
+included version has been heavily modified by Rich Felker in 2012, in
+the interests of size, simplicity, and namespace cleanliness.
+
+Much of the math library code (src/math/* and src/complex/*) is
+Copyright ยฉ 1993,2004 Sun Microsystems or
+Copyright ยฉ 2003-2011 David Schultz or
+Copyright ยฉ 2003-2009 Steven G. Kargl or
+Copyright ยฉ 2003-2009 Bruce D. Evans or
+Copyright ยฉ 2008 Stephen L. Moshier or
+Copyright ยฉ 2017-2018 Arm Limited
+and labelled as such in comments in the individual source files. All
+have been licensed under extremely permissive terms.
+
+The ARM memcpy code (src/string/arm/memcpy.S) is Copyright ยฉ 2008
+The Android Open Source Project and is licensed under a two-clause BSD
+license. It was taken from Bionic libc, used on Android.
+
+The AArch64 memcpy and memset code (src/string/aarch64/*) are
+Copyright ยฉ 1999-2019, Arm Limited.
+
+The implementation of DES for crypt (src/crypt/crypt_des.c) is
+Copyright ยฉ 1994 David Burren. It is licensed under a BSD license.
+
+The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was
+originally written by Solar Designer and placed into the public
+domain. The code also comes with a fallback permissive license for use
+in jurisdictions that may not recognize the public domain.
+
+The smoothsort implementation (src/stdlib/qsort.c) is Copyright ยฉ 2011
+Valentin Ochs and is licensed under an MIT-style license.
+
+The x86_64 port was written by Nicholas J. Kain and is licensed under
+the standard MIT terms.
+
+The mips and microblaze ports were originally written by Richard
+Pennington for use in the ellcc project. The original code was adapted
+by Rich Felker for build system and code conventions during upstream
+integration. It is licensed under the standard MIT terms.
+
+The mips64 port was contributed by Imagination Technologies and is
+licensed under the standard MIT terms.
+
+The powerpc port was also originally written by Richard Pennington,
+and later supplemented and integrated by John Spencer. It is licensed
+under the standard MIT terms.
+
+All other files which have no copyright comments are original works
+produced specifically for use as part of this library, written either
+by Rich Felker, the main author of the library, or by one or more
+contibutors listed above. Details on authorship of individual files
+can be found in the git version control history of the project. The
+omission of copyright and license comments in each file is in the
+interest of source tree size.
+
+In addition, permission is hereby granted for all public header files
+(include/* and arch/*/bits/*) and crt files intended to be linked into
+applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit
+the copyright notice and permission notice otherwise required by the
+license, and to use these files without any requirement of
+attribution. These files include substantial contributions from:
+
+Bobby Bingham
+John Spencer
+Nicholas J. Kain
+Rich Felker
+Richard Pennington
+Stefan Kristiansson
+Szabolcs Nagy
+
+all of whom have explicitly granted such permission.
+
+This file previously contained text expressing a belief that most of
+the files covered by the above exception were sufficiently trivial not
+to be subject to copyright, resulting in confusion over whether it
+negated the permissions granted in the license. In the spirit of
+permissive licensing, and of not having licensing issues being an
+obstacle to adoption, that text has been removed.Copyright (c) 2005-2023, NumPy Developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the NumPy Developers nor the names of any
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+
+Copyright (c) Donald Stufft and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations, which became
+Zope Corporation. In 2001, the Python Software Foundation (PSF, see
+https://www.python.org/psf/) was formed, a non-profit organization
+created specifically to own Python-related Intellectual Property.
+Zope Corporation was a sponsoring member of the PSF.
+
+All Python releases are Open Source (see https://opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+Python software and documentation are licensed under the
+Python Software Foundation License Version 2.
+
+Starting with Python 3.8.6, examples, recipes, and other code in
+the documentation are dual licensed under the PSF License Version 2
+and the Zero-Clause BSD license.
+
+Some software incorporated into Python is under different licenses.
+The licenses are listed with code falling under that license.
+
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
+All Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
+----------------------------------------------------------------------
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+Copyright (c) 2014, Al Sweigart
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the {organization} nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.Copyright (c) 2017 Anthony Sottile
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.Copyright (c) 2015-2019 Jared Hobbs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.Developed by ESN, an Electronic Arts Inc. studio.
+Copyright (c) 2014, Electronic Arts Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+* Neither the name of ESN, Electronic Arts Inc. nor the
+names of its contributors may be used to endorse or promote products
+derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL ELECTRONIC ARTS INC. BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+Portions of code from MODP_ASCII - Ascii transformations (upper/lower, etc)
+https://github.com/client9/stringencoders
+
+ Copyright 2005, 2006, 2007
+ Nick Galbreath -- nickg [at] modp [dot] com
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ Neither the name of the modp.com nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ This is the standard "new" BSD license:
+ http://www.opensource.org/licenses/bsd-license.php
+
+https://github.com/client9/stringencoders/blob/cfd5c1507325ae497ea9bacdacba12c0ffd79d30/COPYING
+
+----
+
+Numeric decoder derived from from TCL library
+https://opensource.apple.com/source/tcl/tcl-14/tcl/license.terms
+ * Copyright (c) 1988-1993 The Regents of the University of California.
+ * Copyright (c) 1994 Sun Microsystems, Inc.
+
+ This software is copyrighted by the Regents of the University of
+ California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
+ Corporation and other parties. The following terms apply to all files
+ associated with the software unless explicitly disclaimed in
+ individual files.
+
+ The authors hereby grant permission to use, copy, modify, distribute,
+ and license this software and its documentation for any purpose, provided
+ that existing copyright notices are retained in all copies and that this
+ notice is included verbatim in any distributions. No written agreement,
+ license, or royalty fee is required for any of the authorized uses.
+ Modifications to this software may be copyrighted by their authors
+ and need not follow the licensing terms described here, provided that
+ the new terms are clearly indicated on the first page of each file where
+ they apply.
+
+ IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
+ FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
+ DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+ THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
+ IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
+ NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
+ MODIFICATIONS.
+
+ GOVERNMENT USE: If you are acquiring this software on behalf of the
+ U.S. government, the Government shall have only "Restricted Rights"
+ in the software and related documentation as defined in the Federal
+ Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
+ are acquiring the software on behalf of the Department of Defense, the
+ software shall be classified as "Commercial Computer Software" and the
+ Government shall have only "Restricted Rights" as defined in Clause
+ 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the
+ authors grant the U.S. Government and others acting in its behalf
+ permission to use and distribute the software in accordance with the
+ terms specified in this license.Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## parso (v0.8.4) - [MIT License](https://github.com/davidhalter/parso)
+
+```
+All contributions towards parso are MIT licensed.
+
+Some Python files have been taken from the standard library and are therefore
+PSF licensed. Modifications on these files are dual licensed (both MIT and
+PSF). These files are:
+
+- parso/pgen2/*
+- parso/tokenize.py
+- parso/token.py
+- test/test_pgen2.py
+
+Also some test files under test/normalizer_issue_files have been copied from
+https://github.com/PyCQA/pycodestyle (Expat License == MIT License).
+
+-------------------------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) <2013-2017>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+-------------------------------------------------------------------------------
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved"
+are retained in Python alone or in any derivative version prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+```
+
+
+## pccm (v0.4.16) - [MIT License](https://github.com/FindDefinition/PCCM)
+
+```
+MIT License
+
+Copyright (c) 2021 FindDefinition
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## pexpect (v4.9.0) - [ISC License (ISCL)](https://pexpect.readthedocs.io/)
+
+```
+ISC LICENSE
+
+ This license is approved by the OSI and FSF as GPL-compatible.
+ http://opensource.org/licenses/isc-license.txt
+
+ Copyright (c) 2013-2014, Pexpect development team
+ Copyright (c) 2012, Noah Spurrier
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+```
+
+
+## pillow (v10.4.0) - [Historical Permission Notice and Disclaimer (HPND)](https://python-pillow.org)
+
+```
+The Python Imaging Library (PIL) is
+
+ Copyright ยฉ 1997-2011 by Secret Labs AB
+ Copyright ยฉ 1995-2011 by Fredrik Lundh and contributors
+
+Pillow is the friendly PIL fork. It is
+
+ Copyright ยฉ 2010-2024 by Jeffrey A. Clark and contributors
+
+Like PIL, Pillow is licensed under the open source HPND License:
+
+By obtaining, using, and/or copying this software and/or its associated
+documentation, you agree that you have read, understood, and will comply
+with the following terms and conditions:
+
+Permission to use, copy, modify and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appears in all copies, and that
+both that copyright notice and this permission notice appear in supporting
+documentation, and that the name of Secret Labs AB or the author not be
+used in advertising or publicity pertaining to distribution of the software
+without specific, written prior permission.
+
+SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
+SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
+IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
+INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+
+----
+
+BROTLI
+
+Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+----
+
+BZIP2
+
+
+--------------------------------------------------------------------------
+
+This program, "bzip2", the associated library "libbzip2", and all
+documentation, are copyright (C) 1996-2019 Julian R Seward. All
+rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Julian Seward, jseward@acm.org
+bzip2/libbzip2 version 1.0.8 of 13 July 2019
+
+--------------------------------------------------------------------------
+
+
+----
+
+FREETYPE2
+
+The FreeType 2 font engine is copyrighted work and cannot be used
+legally without a software license. In order to make this project
+usable to a vast majority of developers, we distribute it under two
+mutually exclusive open-source licenses.
+
+This means that *you* must choose *one* of the two licenses described
+below, then obey all its terms and conditions when using FreeType 2 in
+any of your projects or products.
+
+ - The FreeType License, found in the file `docs/FTL.TXT`, which is
+ similar to the original BSD license *with* an advertising clause
+ that forces you to explicitly cite the FreeType project in your
+ product's documentation. All details are in the license file.
+ This license is suited to products which don't use the GNU General
+ Public License.
+
+ Note that this license is compatible to the GNU General Public
+ License version 3, but not version 2.
+
+ - The GNU General Public License version 2, found in
+ `docs/GPLv2.TXT` (any later version can be used also), for
+ programs which already use the GPL. Note that the FTL is
+ incompatible with GPLv2 due to its advertisement clause.
+
+The contributed BDF and PCF drivers come with a license similar to
+that of the X Window System. It is compatible to the above two
+licenses (see files `src/bdf/README` and `src/pcf/README`). The same
+holds for the source code files `src/base/fthash.c` and
+`include/freetype/internal/fthash.h`; they were part of the BDF driver
+in earlier FreeType versions.
+
+The gzip module uses the zlib license (see `src/gzip/zlib.h`) which
+too is compatible to the above two licenses.
+
+The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code
+taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses
+the 'Old MIT' license, compatible to the above two licenses.
+
+The MD5 checksum support (only used for debugging in development
+builds) is in the public domain.
+
+--------------------------------------------------------------------------
+
+ The FreeType Project LICENSE
+ ----------------------------
+
+ 2006-Jan-27
+
+ Copyright 1996-2002, 2006 by
+ David Turner, Robert Wilhelm, and Werner Lemberg
+
+
+
+Introduction
+============
+
+ The FreeType Project is distributed in several archive packages;
+ some of them may contain, in addition to the FreeType font engine,
+ various tools and contributions which rely on, or relate to, the
+ FreeType Project.
+
+ This license applies to all files found in such packages, and
+ which do not fall under their own explicit license. The license
+ affects thus the FreeType font engine, the test programs,
+ documentation and makefiles, at the very least.
+
+ This license was inspired by the BSD, Artistic, and IJG
+ (Independent JPEG Group) licenses, which all encourage inclusion
+ and use of free software in commercial and freeware products
+ alike. As a consequence, its main points are that:
+
+ o We don't promise that this software works. However, we will be
+ interested in any kind of bug reports. (`as is' distribution)
+
+ o You can use this software for whatever you want, in parts or
+ full form, without having to pay us. (`royalty-free' usage)
+
+ o You may not pretend that you wrote this software. If you use
+ it, or only parts of it, in a program, you must acknowledge
+ somewhere in your documentation that you have used the
+ FreeType code. (`credits')
+
+ We specifically permit and encourage the inclusion of this
+ software, with or without modifications, in commercial products.
+ We disclaim all warranties covering The FreeType Project and
+ assume no liability related to The FreeType Project.
+
+
+ Finally, many people asked us for a preferred form for a
+ credit/disclaimer to use in compliance with this license. We thus
+ encourage you to use the following text:
+
+ """
+ Portions of this software are copyright ยฉ The FreeType
+ Project (www.freetype.org). All rights reserved.
+ """
+
+ Please replace with the value from the FreeType version you
+ actually use.
+
+
+Legal Terms
+===========
+
+0. Definitions
+--------------
+
+ Throughout this license, the terms `package', `FreeType Project',
+ and `FreeType archive' refer to the set of files originally
+ distributed by the authors (David Turner, Robert Wilhelm, and
+ Werner Lemberg) as the `FreeType Project', be they named as alpha,
+ beta or final release.
+
+ `You' refers to the licensee, or person using the project, where
+ `using' is a generic term including compiling the project's source
+ code as well as linking it to form a `program' or `executable'.
+ This program is referred to as `a program using the FreeType
+ engine'.
+
+ This license applies to all files distributed in the original
+ FreeType Project, including all source code, binaries and
+ documentation, unless otherwise stated in the file in its
+ original, unmodified form as distributed in the original archive.
+ If you are unsure whether or not a particular file is covered by
+ this license, you must contact us to verify this.
+
+ The FreeType Project is copyright (C) 1996-2000 by David Turner,
+ Robert Wilhelm, and Werner Lemberg. All rights reserved except as
+ specified below.
+
+1. No Warranty
+--------------
+
+ THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
+ USE, OF THE FREETYPE PROJECT.
+
+2. Redistribution
+-----------------
+
+ This license grants a worldwide, royalty-free, perpetual and
+ irrevocable right and license to use, execute, perform, compile,
+ display, copy, create derivative works of, distribute and
+ sublicense the FreeType Project (in both source and object code
+ forms) and derivative works thereof for any purpose; and to
+ authorize others to exercise some or all of the rights granted
+ herein, subject to the following conditions:
+
+ o Redistribution of source code must retain this license file
+ (`FTL.TXT') unaltered; any additions, deletions or changes to
+ the original files must be clearly indicated in accompanying
+ documentation. The copyright notices of the unaltered,
+ original files must be preserved in all copies of source
+ files.
+
+ o Redistribution in binary form must provide a disclaimer that
+ states that the software is based in part of the work of the
+ FreeType Team, in the distribution documentation. We also
+ encourage you to put an URL to the FreeType web page in your
+ documentation, though this isn't mandatory.
+
+ These conditions apply to any software derived from or based on
+ the FreeType Project, not just the unmodified files. If you use
+ our work, you must acknowledge us. However, no fee need be paid
+ to us.
+
+3. Advertising
+--------------
+
+ Neither the FreeType authors and contributors nor you shall use
+ the name of the other for commercial, advertising, or promotional
+ purposes without specific prior written permission.
+
+ We suggest, but do not require, that you use one or more of the
+ following phrases to refer to this software in your documentation
+ or advertising materials: `FreeType Project', `FreeType Engine',
+ `FreeType library', or `FreeType Distribution'.
+
+ As you have not signed this license, you are not required to
+ accept it. However, as the FreeType Project is copyrighted
+ material, only this license, or another one contracted with the
+ authors, grants you the right to use, distribute, and modify it.
+ Therefore, by using, distributing, or modifying the FreeType
+ Project, you indicate that you understand and accept all the terms
+ of this license.
+
+4. Contacts
+-----------
+
+ There are two mailing lists related to FreeType:
+
+ o freetype@nongnu.org
+
+ Discusses general use and applications of FreeType, as well as
+ future and wanted additions to the library and distribution.
+ If you are looking for support, start in this list if you
+ haven't found anything to help you in the documentation.
+
+ o freetype-devel@nongnu.org
+
+ Discusses bugs, as well as engine internals, design issues,
+ specific licenses, porting, etc.
+
+ Our home page can be found at
+
+ https://www.freetype.org
+
+
+--- end of FTL.TXT ---
+
+--------------------------------------------------------------------------
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
+
+--------------------------------------------------------------------------
+
+The following license details are part of `src/bdf/README`:
+
+```
+License
+*******
+
+Copyright (C) 2001-2002 by Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*** Portions of the driver (that is, bdflib.c and bdf.h):
+
+Copyright 2000 Computing Research Labs, New Mexico State University
+Copyright 2001-2002, 2011 Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Credits
+*******
+
+This driver is based on excellent Mark Leisher's bdf library. If you
+find something good in this driver you should probably thank him, not
+me.
+```
+
+The following license details are part of `src/pcf/README`:
+
+```
+License
+*******
+
+Copyright (C) 2000 by Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Credits
+*******
+
+Keith Packard wrote the pcf driver found in XFree86. His work is at
+the same time the specification and the sample implementation of the
+PCF format. Undoubtedly, this driver is inspired from his work.
+```
+
+
+----
+
+HARFBUZZ
+
+HarfBuzz is licensed under the so-called "Old MIT" license. Details follow.
+For parts of HarfBuzz that are licensed under different licenses see individual
+files names COPYING in subdirectories where applicable.
+
+Copyright ยฉ 2010-2022 Google, Inc.
+Copyright ยฉ 2015-2020 Ebrahim Byagowi
+Copyright ยฉ 2019,2020 Facebook, Inc.
+Copyright ยฉ 2012,2015 Mozilla Foundation
+Copyright ยฉ 2011 Codethink Limited
+Copyright ยฉ 2008,2010 Nokia Corporation and/or its subsidiary(-ies)
+Copyright ยฉ 2009 Keith Stribley
+Copyright ยฉ 2011 Martin Hosken and SIL International
+Copyright ยฉ 2007 Chris Wilson
+Copyright ยฉ 2005,2006,2020,2021,2022,2023 Behdad Esfahbod
+Copyright ยฉ 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc.
+Copyright ยฉ 1998-2005 David Turner and Werner Lemberg
+Copyright ยฉ 2016 Igalia S.L.
+Copyright ยฉ 2022 Matthias Clasen
+Copyright ยฉ 2018,2021 Khaled Hosny
+Copyright ยฉ 2018,2019,2020 Adobe, Inc
+Copyright ยฉ 2013-2015 Alexei Podtelezhnikov
+
+For full copyright notices consult the individual files in the package.
+
+
+Permission is hereby granted, without written agreement and without
+license or royalty fees, to use, copy, modify, and distribute this
+software and its documentation for any purpose, provided that the
+above copyright notice and the following two paragraphs appear in
+all copies of this software.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
+DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
+IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
+PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+
+----
+
+LCMS2
+
+Little CMS
+Copyright (c) 1998-2020 Marti Maria Saguer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+----
+
+LIBJPEG
+
+1. We don't promise that this software works. (But if you find any bugs,
+ please let us know!)
+2. You can use this software for whatever you want. You don't have to pay us.
+3. You may not pretend that you wrote this software. If you use it in a
+ program, you must acknowledge somewhere in your documentation that
+ you've used the IJG code.
+
+In legalese:
+
+The authors make NO WARRANTY or representation, either express or implied,
+with respect to this software, its quality, accuracy, merchantability, or
+fitness for a particular purpose. This software is provided "AS IS", and you,
+its user, assume the entire risk as to its quality and accuracy.
+
+This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding.
+All Rights Reserved except as specified below.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+software (or portions thereof) for any purpose, without fee, subject to these
+conditions:
+(1) If any part of the source code for this software is distributed, then this
+README file must be included, with this copyright and no-warranty notice
+unaltered; and any additions, deletions, or changes to the original files
+must be clearly indicated in accompanying documentation.
+(2) If only executable code is distributed, then the accompanying
+documentation must state that "this software is based in part on the work of
+the Independent JPEG Group".
+(3) Permission for use of this software is granted only if the user accepts
+full responsibility for any undesirable consequences; the authors accept
+NO LIABILITY for damages of any kind.
+
+These conditions apply to any software derived from or based on the IJG code,
+not just to the unmodified library. If you use our work, you ought to
+acknowledge us.
+
+Permission is NOT granted for the use of any IJG author's name or company name
+in advertising or publicity relating to this software or products derived from
+it. This software may be referred to only as "the Independent JPEG Group's
+software".
+
+We specifically permit and encourage the use of this software as the basis of
+commercial products, provided that all warranty or liability claims are
+assumed by the product vendor.
+
+
+----
+
+LIBLZMA
+
+XZ Utils Licensing
+==================
+
+ Different licenses apply to different files in this package. Here
+ is a rough summary of which licenses apply to which parts of this
+ package (but check the individual files to be sure!):
+
+ - liblzma is in the public domain.
+
+ - xz, xzdec, and lzmadec command line tools are in the public
+ domain unless GNU getopt_long had to be compiled and linked
+ in from the lib directory. The getopt_long code is under
+ GNU LGPLv2.1+.
+
+ - The scripts to grep, diff, and view compressed files have been
+ adapted from gzip. These scripts and their documentation are
+ under GNU GPLv2+.
+
+ - All the documentation in the doc directory and most of the
+ XZ Utils specific documentation files in other directories
+ are in the public domain.
+
+ - Translated messages are in the public domain.
+
+ - The build system contains public domain files, and files that
+ are under GNU GPLv2+ or GNU GPLv3+. None of these files end up
+ in the binaries being built.
+
+ - Test files and test code in the tests directory, and debugging
+ utilities in the debug directory are in the public domain.
+
+ - The extra directory may contain public domain files, and files
+ that are under various free software licenses.
+
+ You can do whatever you want with the files that have been put into
+ the public domain. If you find public domain legally problematic,
+ take the previous sentence as a license grant. If you still find
+ the lack of copyright legally problematic, you have too many
+ lawyers.
+
+ As usual, this software is provided "as is", without any warranty.
+
+ If you copy significant amounts of public domain code from XZ Utils
+ into your project, acknowledging this somewhere in your software is
+ polite (especially if it is proprietary, non-free software), but
+ naturally it is not legally required. Here is an example of a good
+ notice to put into "about box" or into documentation:
+
+ This software includes code from XZ Utils .
+
+ The following license texts are included in the following files:
+ - COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1
+ - COPYING.GPLv2: GNU General Public License version 2
+ - COPYING.GPLv3: GNU General Public License version 3
+
+ Note that the toolchain (compiler, linker etc.) may add some code
+ pieces that are copyrighted. Thus, it is possible that e.g. liblzma
+ binary wouldn't actually be in the public domain in its entirety
+ even though it contains no copyrighted code from the XZ Utils source
+ package.
+
+ If you have questions, don't hesitate to ask the author(s) for more
+ information.
+
+
+----
+
+LIBPNG
+
+COPYRIGHT NOTICE, DISCLAIMER, and LICENSE
+=========================================
+
+PNG Reference Library License version 2
+---------------------------------------
+
+ * Copyright (c) 1995-2022 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2022 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement. In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+ must not claim that you wrote the original software. If you
+ use this software in a product, an acknowledgment in the product
+ documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+
+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
+-----------------------------------------------------------------------
+
+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
+derived from libpng-1.0.6, and are distributed according to the same
+disclaimer and license as libpng-1.0.6 with the following individuals
+added to the list of Contributing Authors:
+
+ Simon-Pierre Cadieux
+ Eric S. Raymond
+ Mans Rullgard
+ Cosmin Truta
+ Gilles Vollant
+ James Yu
+ Mandar Sahastrabuddhe
+ Google Inc.
+ Vadim Barkov
+
+and with the following additions to the disclaimer:
+
+ There is no warranty against interference with your enjoyment of
+ the library or against infringement. There is no warranty that our
+ efforts or the library will fulfill any of your particular purposes
+ or needs. This library is provided with all faults, and the entire
+ risk of satisfactory quality, performance, accuracy, and effort is
+ with the user.
+
+Some files in the "contrib" directory and some configure-generated
+files that are distributed with libpng have other copyright owners, and
+are released under other open source licenses.
+
+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
+libpng-0.96, and are distributed according to the same disclaimer and
+license as libpng-0.96, with the following individuals added to the
+list of Contributing Authors:
+
+ Tom Lane
+ Glenn Randers-Pehrson
+ Willem van Schaik
+
+libpng versions 0.89, June 1996, through 0.96, May 1997, are
+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
+and are distributed according to the same disclaimer and license as
+libpng-0.88, with the following individuals added to the list of
+Contributing Authors:
+
+ John Bowler
+ Kevin Bracey
+ Sam Bushell
+ Magnus Holmgren
+ Greg Roelofs
+ Tom Tanner
+
+Some files in the "scripts" directory have other copyright owners,
+but are released under this license.
+
+libpng versions 0.5, May 1995, through 0.88, January 1996, are
+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+For the purposes of this copyright and license, "Contributing Authors"
+is defined as the following set of individuals:
+
+ Andreas Dilger
+ Dave Martindale
+ Guy Eric Schalnat
+ Paul Schmidt
+ Tim Wegner
+
+The PNG Reference Library is supplied "AS IS". The Contributing
+Authors and Group 42, Inc. disclaim all warranties, expressed or
+implied, including, without limitation, the warranties of
+merchantability and of fitness for any purpose. The Contributing
+Authors and Group 42, Inc. assume no liability for direct, indirect,
+incidental, special, exemplary, or consequential damages, which may
+result from the use of the PNG Reference Library, even if advised of
+the possibility of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+source code, or portions hereof, for any purpose, without fee, subject
+to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented.
+
+ 2. Altered versions must be plainly marked as such and must not
+ be misrepresented as being the original source.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+The Contributing Authors and Group 42, Inc. specifically permit,
+without fee, and encourage the use of this source code as a component
+to supporting the PNG file format in commercial products. If you use
+this source code in a product, acknowledgment is not required but would
+be appreciated.
+
+
+----
+
+LIBTIFF
+
+Copyright (c) 1988-1997 Sam Leffler
+Copyright (c) 1991-1997 Silicon Graphics, Inc.
+
+Permission to use, copy, modify, distribute, and sell this software and
+its documentation for any purpose is hereby granted without fee, provided
+that (i) the above copyright notices and this permission notice appear in
+all copies of the software and related documentation, and (ii) the names of
+Sam Leffler and Silicon Graphics may not be used in any advertising or
+publicity relating to the software without the specific, prior written
+permission of Sam Leffler and Silicon Graphics.
+
+THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
+
+IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
+ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
+LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+OF THIS SOFTWARE.
+
+
+----
+
+LIBWEBP
+
+Copyright (c) 2010, Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+----
+
+OPENJPEG
+
+*
+ * The copyright in this software is being made available under the 2-clauses
+ * BSD License, included below. This software may be subject to other third
+ * party and contributor rights, including patent rights, and no such rights
+ * are granted under this license.
+ *
+ * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium
+ * Copyright (c) 2002-2014, Professor Benoit Macq
+ * Copyright (c) 2003-2014, Antonin Descampe
+ * Copyright (c) 2003-2009, Francois-Olivier Devaux
+ * Copyright (c) 2005, Herve Drolon, FreeImage Team
+ * Copyright (c) 2002-2003, Yannick Verschueren
+ * Copyright (c) 2001-2003, David Janssens
+ * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France
+ * Copyright (c) 2012, CS Systemes d'Information, France
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+----
+
+RAQM
+
+The MIT License (MIT)
+
+Copyright ยฉ 2015 Information Technology Authority (ITA)
+Copyright ยฉ 2016 Khaled Hosny
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+----
+
+XAU
+
+Copyright 1988, 1993, 1994, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
+
+
+----
+
+XCB
+
+Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett.
+All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall
+be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors
+or their institutions shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this
+Software without prior written authorization from the
+authors.
+
+
+----
+
+XDMCP
+
+Copyright 1989, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
+
+Author: Keith Packard, MIT X Consortium
+
+
+----
+
+ZLIB
+
+ (C) 1995-2017 Jean-loup Gailly and Mark Adler
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ Jean-loup Gailly Mark Adler
+ jloup@gzip.org madler@alumni.caltech.edu
+
+If you use the zlib library in a product, we would appreciate *not* receiving
+lengthy legal documents to sign. The sources are provided for free but without
+warranty of any kind. The library has been entirely written by Jean-loup
+Gailly and Mark Adler; it does not include third-party code.
+
+If you redistribute modified sources, we would appreciate that you include in
+the file ChangeLog history information documenting your changes. Please read
+the FAQ for more information on the distribution of modified source versions.
+```
+
+
+## platformdirs (v4.2.2) - [MIT License](https://github.com/platformdirs/platformdirs)
+
+```
+MIT License
+
+Copyright (c) 2010-202x The platformdirs developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## plotly (v5.24.1) - [MIT License](https://plotly.com/python/)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2016-2018 Plotly, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## pluggy (v1.6.0) - [MIT License](https://opensource.org/licenses/MIT)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## polyscope (v2.4.0) - [MIT License](https://github.com/nmwsharp/polyscope)
+
+```
+MIT License
+
+Copyright (c) 2017-2020 Nicholas Sharp and the Polyscope contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## portalocker (v3.1.1) - [BSD License](https://github.com/wolph/portalocker/)
+
+```
+Copyright 2022 Rick van Hattem
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## prompt_toolkit (v3.0.50) - [BSD License](https://github.com/prompt-toolkit/python-prompt-toolkit)
+
+```
+Copyright (c) 2014, Jonathan Slenders
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+* Neither the name of the {organization} nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## psutil (v7.0.0) - [BSD License](https://github.com/giampaolo/psutil)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name of the psutil authors nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## ptyprocess (v0.7.0) - [ISC License (ISCL)](https://github.com/pexpect/ptyprocess)
+
+```
+Ptyprocess is under the ISC license, as code derived from Pexpect.
+ http://opensource.org/licenses/ISC
+
+Copyright (c) 2013-2014, Pexpect development team
+Copyright (c) 2012, Noah Spurrier
+
+PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE
+WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE COPYRIGHT NOTICE
+AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. THE SOFTWARE IS PROVIDED
+"AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
+INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT
+SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+```
+
+
+## pure_eval (v0.2.3) - [MIT License](http://github.com/alexmojaki/pure_eval)
+
+```
+MIT License
+
+Copyright (c) 2019 Alex Hall
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## pybind11 (v2.13.6) - [BSD License](https://github.com/pybind/pybind11)
+
+```
+Copyright (c) 2016 Wenzel Jakob , All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of
+external contributions to this project including patches, pull requests, etc.
+```
+
+
+## pybullet (v3.2.7) - [zlib/libpng License](https://github.com/bulletphysics/bullet3)
+
+```
+The files in this repository are licensed under the zlib license, except for the files under 'Extras' and examples/ThirdPartyLibs.
+
+Bullet Continuous Collision Detection and Physics Library
+http://bulletphysics.org
+
+This software is provided 'as-is', without any express or implied warranty.
+In no event will the authors be held liable for any damages arising from the use of this software.
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it freely,
+subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+```
+
+
+## pydantic (v2.7.1) - [MIT License](https://github.com/pydantic/pydantic)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## pydantic_core (v2.18.2) - [MIT License](https://github.com/pydantic/pydantic-core)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2022 Samuel Colvin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## pygltflib (v1.16.0) - [MIT License](https://gitlab.com/dodgyville/pygltflib)
+
+```
+Copyright (c) 2018 Luke Miller
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## Pygments (v2.19.1) - [BSD License](https://pygments.org)
+
+```
+Copyright (c) 2006-2022 by the respective authors (see AUTHORS file).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## pyparsing (v3.2.3) - [MIT License](https://github.com/pyparsing/pyparsing/)
+
+```
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## pyquaternion (v0.9.9) - [MIT License](http://kieranwynn.github.io/pyquaternion/)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2015 Kieran Wynn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## pytest (v8.3.5) - [MIT License](https://docs.pytest.org/en/latest/)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2004 Holger Krekel and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## python-dateutil (v2.9.0.post0) - [Apache Software License; BSD License](https://github.com/dateutil/dateutil)
+
+```
+Copyright 2017- Paul Ganssle
+Copyright 2017- dateutil contributors (see AUTHORS file)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+The above license applies to all contributions after 2017-12-01, as well as
+all contributions that have been re-licensed (see AUTHORS file for the list of
+contributors who have re-licensed their code).
+--------------------------------------------------------------------------------
+dateutil - Extensions to the standard Python datetime module.
+
+Copyright (c) 2003-2011 - Gustavo Niemeyer
+Copyright (c) 2012-2014 - Tomi Pievilรคinen
+Copyright (c) 2014-2016 - Yaron de Leeuw
+Copyright (c) 2015- - Paul Ganssle
+Copyright (c) 2015- - dateutil contributors (see AUTHORS file)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The above BSD License Applies to all code, even that also covered by Apache 2.0.
+```
+
+
+## pytz (v2025.2) - [MIT License](http://pythonhosted.org/pytz)
+
+```
+Copyright (c) 2003-2019 Stuart Bishop
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+```
+
+
+## PyYAML (v6.0.2) - [MIT License](https://pyyaml.org/)
+
+```
+Copyright (c) 2017-2021 Ingy dรถt Net
+Copyright (c) 2006-2016 Kirill Simonov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## pyzmq (v26.3.0) - [BSD License](https://pyzmq.readthedocs.org)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2009-2012, Brian Granger, Min Ragan-Kelley
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## referencing (v0.36.2) - [MIT License](https://github.com/python-jsonschema/referencing)
+
+```
+Copyright (c) 2022 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## regex (v2024.11.6) - [Apache License 2.0](https://github.com/mrabarnett/mrab-regex)
+
+```
+This work was derived from the 're' module of CPython 2.6 and CPython 3.1,
+copyright (c) 1998-2001 by Secret Labs AB and licensed under CNRI's Python 1.6
+license.
+
+All additions and alterations are licensed under the Apache 2.0 License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2020 Matthew Barnett
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## requests (v2.32.3) - [Apache License 2.0](https://requests.readthedocs.io)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+```
+
+
+## retrying (v1.3.4) - [Apache License 2.0](https://github.com/groodt/retrying)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## rpds-py (v0.24.0) - [MIT License](https://github.com/crate-py/rpds)
+
+```
+Copyright (c) 2023 Julian Berman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## scikit-learn (v1.6.1) - [BSD License](https://scikit-learn.org)
+
+```
+BSD 3-Clause License
+
+Copyright (c) 2007-2024 The scikit-learn developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+This binary distribution of scikit-learn also bundles the following software:
+
+----
+
+Name: GCC runtime library
+Files: scikit_learn.libs/libgomp*.so*
+Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgomp
+
+GCC RUNTIME LIBRARY EXCEPTION
+
+Version 3.1, 31 March 2009
+
+Copyright (C) 2009 Free Software Foundation, Inc.
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+This GCC Runtime Library Exception ("Exception") is an additional
+permission under section 7 of the GNU General Public License, version
+3 ("GPLv3"). It applies to a given file (the "Runtime Library") that
+bears a notice placed by the copyright holder of the file stating that
+the file is governed by GPLv3 along with this Exception.
+
+When you use GCC to compile a program, GCC may combine portions of
+certain GCC header files and runtime libraries with the compiled
+program. The purpose of this Exception is to allow compilation of
+non-GPL (including proprietary) programs to use, in this way, the
+header files and runtime libraries covered by this Exception.
+
+0. Definitions.
+
+A file is an "Independent Module" if it either requires the Runtime
+Library for execution after a Compilation Process, or makes use of an
+interface provided by the Runtime Library, but is not otherwise based
+on the Runtime Library.
+
+"GCC" means a version of the GNU Compiler Collection, with or without
+modifications, governed by version 3 (or a specified later version) of
+the GNU General Public License (GPL) with the option of using any
+subsequent versions published by the FSF.
+
+"GPL-compatible Software" is software whose conditions of propagation,
+modification and use would permit combination with GCC in accord with
+the license of GCC.
+
+"Target Code" refers to output from any compiler for a real or virtual
+target processor architecture, in executable form or suitable for
+input to an assembler, loader, linker and/or execution
+phase. Notwithstanding that, Target Code does not include data in any
+format that is used as a compiler intermediate representation, or used
+for producing a compiler intermediate representation.
+
+The "Compilation Process" transforms code entirely represented in
+non-intermediate languages designed for human-written code, and/or in
+Java Virtual Machine byte code, into Target Code. Thus, for example,
+use of source code generators and preprocessors need not be considered
+part of the Compilation Process, since the Compilation Process can be
+understood as starting with the output of the generators or
+preprocessors.
+
+A Compilation Process is "Eligible" if it is done using GCC, alone or
+with other GPL-compatible software, or if it is done without using any
+work based on GCC. For example, using non-GPL-compatible Software to
+optimize any GCC intermediate representations would not qualify as an
+Eligible Compilation Process.
+
+1. Grant of Additional Permission.
+
+You have permission to propagate a work of Target Code formed by
+combining the Runtime Library with Independent Modules, even if such
+propagation would otherwise violate the terms of GPLv3, provided that
+all Target Code was generated by Eligible Compilation Processes. You
+may then convey such a combination under terms of your choice,
+consistent with the licensing of the Independent Modules.
+
+2. No Weakening of GCC Copyleft.
+
+The availability of this Exception does not imply any general
+presumption that third-party software is unaffected by the copyleft
+requirements of the license of GCC.
+```
+
+
+## scipy (v1.15.2) - [BSD License](https://scipy.org/)
+
+```
+Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+This binary distribution of SciPy can also bundle the following software
+(depending on the build):
+
+
+Name: OpenBLAS
+Files: scipy.libs/libscipy_openblas*.so
+Description: bundled as a dynamically linked library
+Availability: https://github.com/OpenMathLib/OpenBLAS/
+License: BSD-3-Clause-Attribution
+ Copyright (c) 2011-2014, The OpenBLAS Project
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ 3. Neither the name of the OpenBLAS project nor the names of
+ its contributors may be used to endorse or promote products
+ derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+Name: LAPACK
+Files: scipy.libs/libscipy_openblas*.so
+Description: bundled in OpenBLAS
+Availability: https://github.com/OpenMathLib/OpenBLAS/
+License: BSD-3-Clause-Attribution
+ Copyright (c) 1992-2013 The University of Tennessee and The University
+ of Tennessee Research Foundation. All rights
+ reserved.
+ Copyright (c) 2000-2013 The University of California Berkeley. All
+ rights reserved.
+ Copyright (c) 2006-2013 The University of Colorado Denver. All rights
+ reserved.
+
+ $COPYRIGHT$
+
+ Additional copyrights may follow
+
+ $HEADER$
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ - Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer listed
+ in this license in the documentation and/or other materials
+ provided with the distribution.
+
+ - Neither the name of the copyright holders nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ The copyright holders provide no reassurances that the source code
+ provided does not infringe any patent, copyright, or any other
+ intellectual property rights of third parties. The copyright holders
+ disclaim any liability to any recipient for claims brought against
+ recipient by any third party for infringement of that parties
+ intellectual property rights.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+Name: GCC runtime library
+Files: scipy.libs/libgfortran*.so
+Description: dynamically linked to files compiled with gcc
+Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran
+License: GPL-3.0-with-GCC-exception
+ Copyright (C) 2002-2017 Free Software Foundation, Inc.
+
+ Libgfortran is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3, or (at your option)
+ any later version.
+
+ Libgfortran is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ Under Section 7 of GPL version 3, you are granted additional
+ permissions described in the GCC Runtime Library Exception, version
+ 3.1, as published by the Free Software Foundation.
+
+ You should have received a copy of the GNU General Public License and
+ a copy of the GCC Runtime Library Exception along with this program;
+ see the files COPYING3 and COPYING.RUNTIME respectively. If not, see
+ .
+
+----
+
+Full text of license texts referred to above follows (that they are
+listed below does not necessarily imply the conditions apply to the
+present binary release):
+
+----
+
+GCC RUNTIME LIBRARY EXCEPTION
+
+Version 3.1, 31 March 2009
+
+Copyright (C) 2009 Free Software Foundation, Inc.
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+This GCC Runtime Library Exception ("Exception") is an additional
+permission under section 7 of the GNU General Public License, version
+3 ("GPLv3"). It applies to a given file (the "Runtime Library") that
+bears a notice placed by the copyright holder of the file stating that
+the file is governed by GPLv3 along with this Exception.
+
+When you use GCC to compile a program, GCC may combine portions of
+certain GCC header files and runtime libraries with the compiled
+program. The purpose of this Exception is to allow compilation of
+non-GPL (including proprietary) programs to use, in this way, the
+header files and runtime libraries covered by this Exception.
+
+0. Definitions.
+
+A file is an "Independent Module" if it either requires the Runtime
+Library for execution after a Compilation Process, or makes use of an
+interface provided by the Runtime Library, but is not otherwise based
+on the Runtime Library.
+
+"GCC" means a version of the GNU Compiler Collection, with or without
+modifications, governed by version 3 (or a specified later version) of
+the GNU General Public License (GPL) with the option of using any
+subsequent versions published by the FSF.
+
+"GPL-compatible Software" is software whose conditions of propagation,
+modification and use would permit combination with GCC in accord with
+the license of GCC.
+
+"Target Code" refers to output from any compiler for a real or virtual
+target processor architecture, in executable form or suitable for
+input to an assembler, loader, linker and/or execution
+phase. Notwithstanding that, Target Code does not include data in any
+format that is used as a compiler intermediate representation, or used
+for producing a compiler intermediate representation.
+
+The "Compilation Process" transforms code entirely represented in
+non-intermediate languages designed for human-written code, and/or in
+Java Virtual Machine byte code, into Target Code. Thus, for example,
+use of source code generators and preprocessors need not be considered
+part of the Compilation Process, since the Compilation Process can be
+understood as starting with the output of the generators or
+preprocessors.
+
+A Compilation Process is "Eligible" if it is done using GCC, alone or
+with other GPL-compatible software, or if it is done without using any
+work based on GCC. For example, using non-GPL-compatible Software to
+optimize any GCC intermediate representations would not qualify as an
+Eligible Compilation Process.
+
+1. Grant of Additional Permission.
+
+You have permission to propagate a work of Target Code formed by
+combining the Runtime Library with Independent Modules, even if such
+propagation would otherwise violate the terms of GPLv3, provided that
+all Target Code was generated by Eligible Compilation Processes. You
+may then convey such a combination under terms of your choice,
+consistent with the licensing of the Independent Modules.
+
+2. No Weakening of GCC Copyleft.
+
+The availability of this Exception does not imply any general
+presumption that third-party software is unaffected by the copyleft
+requirements of the license of GCC.
+
+----
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
+
+
+Name: libquadmath
+Files: scipy.libs/libquadmath*.so
+Description: dynamically linked to files compiled with gcc
+Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath
+License: LGPL-2.1-or-later
+
+ GCC Quad-Precision Math Library
+ Copyright (C) 2010-2019 Free Software Foundation, Inc.
+ Written by Francois-Xavier Coudert
+
+ This file is part of the libquadmath library.
+ Libquadmath is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ Libquadmath is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+ https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+```
+
+
+## six (v1.17.0) - [MIT License](https://github.com/benjaminp/six)
+
+```
+Copyright (c) 2010-2024 Benjamin Peterson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## sniffio (v1.3.1) - [Apache Software License; MIT License](https://github.com/python-trio/sniffio)
+
+```
+This software is made available under the terms of *either* of the
+licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are
+made under the terms of *both* these licenses.
+```
+
+
+## spconv-cu118 (v2.3.8) - [Apache License 2.0](https://github.com/traveller59/spconv)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2019-2021 Yan Yan
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## stack-data (v0.6.3) - [MIT License](http://github.com/alexmojaki/stack_data)
+
+```
+MIT License
+
+Copyright (c) 2019 Alex Hall
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## sympy (v1.13.1) - [BSD License](https://sympy.org)
+
+```
+Copyright (c) 2006-2023 SymPy Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ a. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ b. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ c. Neither the name of SymPy nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+--------------------------------------------------------------------------------
+
+Patches that were taken from the Diofant project (https://github.com/diofant/diofant)
+are licensed as:
+
+Copyright (c) 2006-2018 SymPy Development Team,
+ 2013-2023 Sergey B Kirpichev
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ a. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ b. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ c. Neither the name of Diofant or SymPy nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+--------------------------------------------------------------------------------
+
+Submodules taken from the multipledispatch project (https://github.com/mrocklin/multipledispatch)
+are licensed as:
+
+Copyright (c) 2014 Matthew Rocklin
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ a. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ b. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ c. Neither the name of multipledispatch nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+--------------------------------------------------------------------------------
+
+The files under the directory sympy/parsing/autolev/tests/pydy-example-repo
+are directly copied from PyDy project and are licensed as:
+
+Copyright (c) 2009-2023, PyDy Authors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+* Neither the name of this project nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL PYDY AUTHORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+--------------------------------------------------------------------------------
+
+The files under the directory sympy/parsing/latex
+are directly copied from latex2sympy project and are licensed as:
+
+Copyright 2016, latex2sympy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## tensorboard (v2.19.0) - [Apache License 2.0](https://github.com/tensorflow/tensorboard)
+
+```
+# TensorBoard License
+
+TensorBoard is licensed Apache 2.0 and distributed with
+vendored content licensed Apache 2.0, MIT, and BSD-3.
+
+## Table of Contents
+
+- tensorboard/pip_package/LICENSE.tensorflow
+- external/npm/node_modules/d3/LICENSE
+- external/com_google_fonts_roboto/LICENSE
+- external/org_mozilla_bleach/LICENSE
+- external/org_html5lib/LICENSE
+- external/org_pythonhosted_webencodings/LICENSE
+- third_party/bh_tsne.LICENSE
+
+## Licenses
+
+
+
+### tensorboard/pip_package/LICENSE.tensorflow
+
+Copyright 2017 The TensorFlow Authors. All rights reserved.
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017, The TensorFlow Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+### external/npm/node_modules/d3/LICENSE
+
+Copyright 2010-2017 Mike Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the author nor the names of contributors may be used to
+ endorse or promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+### external/com_google_fonts_roboto/LICENSE
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+### external/org_mozilla_bleach/LICENSE
+
+Copyright (c) 2014-2017, Mozilla Foundation
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
+### external/org_html5lib/LICENSE
+
+Copyright (c) 2006-2013 James Graham and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+
+### external/org_pythonhosted_webencodings/LICENSE
+
+Copyright (c) 2012 by Simon Sapin.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+### third_party/bh_tsne.LICENSE
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Andrej Karpathy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## termcolor (v3.0.1) - [MIT License](https://github.com/termcolor/termcolor)
+
+```
+Copyright (c) 2008-2011 Volvox Development Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+## threadpoolctl (v3.6.0) - [BSD License](https://github.com/joblib/threadpoolctl)
+
+```
+Copyright (c) 2019, threadpoolctl contributors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## torch (v2.5.1+cu118) - [BSD License](https://pytorch.org/)
+
+```
+From PyTorch:
+
+Copyright (c) 2016- Facebook, Inc (Adam Paszke)
+Copyright (c) 2014- Facebook, Inc (Soumith Chintala)
+Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert)
+Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu)
+Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu)
+Copyright (c) 2011-2013 NYU (Clement Farabet)
+Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston)
+Copyright (c) 2006 Idiap Research Institute (Samy Bengio)
+Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz)
+
+From Caffe2:
+
+Copyright (c) 2016-present, Facebook Inc. All rights reserved.
+
+All contributions by Facebook:
+Copyright (c) 2016 Facebook Inc.
+
+All contributions by Google:
+Copyright (c) 2015 Google Inc.
+All rights reserved.
+
+All contributions by Yangqing Jia:
+Copyright (c) 2015 Yangqing Jia
+All rights reserved.
+
+All contributions by Kakao Brain:
+Copyright 2019-2020 Kakao Brain
+
+All contributions by Cruise LLC:
+Copyright (c) 2022 Cruise LLC.
+All rights reserved.
+
+All contributions by Arm:
+Copyright (c) 2021, 2023-2024 Arm Limited and/or its affiliates
+
+All contributions from Caffe:
+Copyright(c) 2013, 2014, 2015, the respective contributors
+All rights reserved.
+
+All other contributions:
+Copyright(c) 2015, 2016 the respective contributors
+All rights reserved.
+
+Caffe2 uses a copyright model similar to Caffe: each contributor holds
+copyright over their contributions to Caffe2. The project versioning records
+all such contribution and copyright details. If a contributor wants to further
+mark their specific copyright on a particular contribution, they should
+indicate their copyright solely in the commit message of the change when it is
+committed.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America
+ and IDIAP Research Institute nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+
+The PyTorch repository and source distributions bundle several libraries that are
+compatibly licensed. We list these here.
+
+Name: DCGM
+License: Apache-2.0
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/LICENSE
+
+Name: FP16
+License: MIT
+Files: third_party/FP16
+ For details, see the files concatenated below: third_party/FP16/LICENSE
+
+Name: FXdiv
+License: MIT
+Files: third_party/FXdiv
+ For details, see the files concatenated below: third_party/FXdiv/LICENSE
+
+Name: NNPACK
+License: BSD-2-Clause
+Files: third_party/NNPACK
+ For details, see the files concatenated below: third_party/NNPACK/LICENSE
+
+Name: NVTX
+License: Apache-2.0 with exception
+Files: third_party/NVTX
+ For details, see the files concatenated below: third_party/NVTX/LICENSE.txt
+
+Name: VulkanMemoryAllocator
+License: MIT
+Files: third_party/VulkanMemoryAllocator
+ For details, see the files concatenated below: third_party/VulkanMemoryAllocator/LICENSE.txt
+
+Name: XNNPACK
+License: BSD-3-Clause
+Files: third_party/XNNPACK
+ For details, see the files concatenated below: third_party/XNNPACK/LICENSE
+
+Name: benchmark
+License: Apache-2.0
+Files: third_party/benchmark,
+ third_party/onnx/third_party/benchmark,
+ third_party/opentelemetry-cpp/third_party/benchmark,
+ third_party/protobuf/third_party/benchmark
+ For details, see the files concatenated below: third_party/benchmark/LICENSE,
+ third_party/onnx/third_party/benchmark/LICENSE,
+ third_party/opentelemetry-cpp/third_party/benchmark/LICENSE,
+ third_party/protobuf/third_party/benchmark/LICENSE
+
+Name: boost-vcpkg-helpers
+License: MIT
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/boost-vcpkg-helpers
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/boost-vcpkg-helpers/LICENSE.txt
+
+Name: cJSON
+License: MIT
+Files: third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON/LICENSE
+
+Name: catch2
+License: BSL-1.0
+Files: third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/catch2
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/catch2/LICENSE.txt
+
+Name: clog
+License: BSD-2-Clause
+Files: third_party/cpuinfo/deps/clog,
+ third_party/fbgemm/third_party/cpuinfo/deps/clog
+ For details, see the files concatenated below: third_party/cpuinfo/deps/clog/LICENSE,
+ third_party/fbgemm/third_party/cpuinfo/deps/clog/LICENSE
+
+Name: colorama
+License: BSD-3-Clause
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/testing/python3/libs_3rdparty/colorama
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/testing/python3/libs_3rdparty/colorama/LICENSE.txt
+
+Name: cpp-httplib
+License: MIT
+Files: third_party/cpp-httplib
+ For details, see the files concatenated below: third_party/cpp-httplib/LICENSE
+
+Name: cpplint
+License: BSD-3-Clause
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/json/third_party/cpplint,
+ third_party/nlohmann/tools/cpplint
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/json/third_party/cpplint/LICENSE,
+ third_party/nlohmann/tools/cpplint/LICENSE
+
+Name: cpr
+License: MIT
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/cpr
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/LICENSE
+
+Name: cpuinfo
+License: BSD-2-Clause
+Files: third_party/cpuinfo,
+ third_party/fbgemm/third_party/cpuinfo
+ For details, see the files concatenated below: third_party/cpuinfo/LICENSE,
+ third_party/fbgemm/third_party/cpuinfo/LICENSE
+
+Name: cudnn_frontend
+License: MIT
+Files: third_party/cudnn_frontend
+ For details, see the files concatenated below: third_party/cudnn_frontend/LICENSE.txt
+
+Name: cutlass
+License: BSD-3-Clause
+Files: third_party/cutlass,
+ third_party/fbgemm/third_party/cutlass
+ For details, see the files concatenated below: third_party/cutlass/LICENSE.txt,
+ third_party/fbgemm/third_party/cutlass/LICENSE.txt
+
+Name: dart
+License: Apache-2.0
+Files: third_party/flatbuffers/dart
+ For details, see the files concatenated below: third_party/flatbuffers/dart/LICENSE
+
+Name: doctest
+License: MIT
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/json/test/thirdparty/doctest,
+ third_party/nlohmann/tests/thirdparty/doctest
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/json/test/thirdparty/doctest/LICENSE.txt,
+ third_party/nlohmann/tests/thirdparty/doctest/LICENSE.txt
+
+Name: duktape-1.5.2
+License: MIT
+Files: third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2/LICENSE.txt
+
+Name: duktape-1.8.0
+License: MIT
+Files: third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0/LICENSE.txt
+
+Name: dynolog
+License: MIT
+Files: third_party/kineto/libkineto/third_party/dynolog
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/LICENSE
+
+Name: eigen
+License: BSD-3-Clause
+Files: third_party/eigen
+ For details, see the files concatenated below: third_party/eigen/COPYING.BSD
+
+Name: etw
+License: MIT
+Files: third_party/opentelemetry-cpp/exporters/etw/include/opentelemetry/exporters/etw
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/exporters/etw/include/opentelemetry/exporters/etw/LICENSE
+
+Name: expected
+License: MIT
+Files: third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/expected
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/expected/LICENSE
+
+Name: fbgemm
+License: BSD-3-Clause
+Files: third_party/fbgemm
+ For details, see the files concatenated below: third_party/fbgemm/LICENSE
+
+Name: ffnvcodec
+License: MIT with exception
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/ffnvcodec
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/ffnvcodec/LICENSE.txt
+
+Name: flatbuffers
+License: Apache-2.0
+Files: third_party/flatbuffers
+ For details, see the files concatenated below: third_party/flatbuffers/LICENSE
+
+Name: fmt
+License: MIT with exception
+Files: third_party/fmt,
+ third_party/kineto/libkineto/third_party/dynolog/third_party/fmt,
+ third_party/kineto/libkineto/third_party/fmt
+ For details, see the files concatenated below: third_party/fmt/LICENSE,
+ third_party/kineto/libkineto/third_party/dynolog/third_party/fmt/LICENSE.rst,
+ third_party/kineto/libkineto/third_party/fmt/LICENSE
+
+Name: gemmlowp
+License: Apache-2.0
+Files: third_party/gemmlowp/gemmlowp
+ For details, see the files concatenated below: third_party/gemmlowp/gemmlowp/LICENSE
+
+Name: generator
+License: Apache-2.0
+Files: third_party/fbgemm/third_party/googletest/googlemock/scripts/generator,
+ third_party/googletest/googlemock/scripts/generator,
+ third_party/kineto/libkineto/third_party/googletest/googlemock/scripts/generator,
+ third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator,
+ third_party/protobuf/third_party/googletest/googlemock/scripts/generator,
+ third_party/tensorpipe/third_party/googletest/googlemock/scripts/generator
+ For details, see the files concatenated below: third_party/fbgemm/third_party/googletest/googlemock/scripts/generator/LICENSE,
+ third_party/googletest/googlemock/scripts/generator/LICENSE,
+ third_party/kineto/libkineto/third_party/googletest/googlemock/scripts/generator/LICENSE,
+ third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator/LICENSE,
+ third_party/protobuf/third_party/googletest/googlemock/scripts/generator/LICENSE,
+ third_party/tensorpipe/third_party/googletest/googlemock/scripts/generator/LICENSE
+
+Name: gettimeofday
+License: Apache-2.0
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/gettimeofday
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/gettimeofday/LICENSE
+
+Name: gloo
+License: BSD-3-Clause
+Files: third_party/gloo
+ For details, see the files concatenated below: third_party/gloo/LICENSE
+
+Name: googlemock
+License: BSD-3-Clause
+Files: third_party/fbgemm/third_party/googletest/googlemock,
+ third_party/kineto/libkineto/third_party/googletest/googlemock,
+ third_party/protobuf/third_party/googletest/googlemock,
+ third_party/tensorpipe/third_party/googletest/googlemock
+ For details, see the files concatenated below: third_party/fbgemm/third_party/googletest/googlemock/LICENSE,
+ third_party/kineto/libkineto/third_party/googletest/googlemock/LICENSE,
+ third_party/protobuf/third_party/googletest/googlemock/LICENSE,
+ third_party/tensorpipe/third_party/googletest/googlemock/LICENSE
+
+Name: googletest
+License: BSD-3-Clause
+Files: third_party/fbgemm/third_party/googletest,
+ third_party/fbgemm/third_party/googletest/googletest,
+ third_party/googletest,
+ third_party/kineto/libkineto/third_party/dynolog/third_party/googletest,
+ third_party/kineto/libkineto/third_party/googletest,
+ third_party/kineto/libkineto/third_party/googletest/googletest,
+ third_party/opentelemetry-cpp/third_party/googletest,
+ third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest,
+ third_party/protobuf/third_party/googletest,
+ third_party/protobuf/third_party/googletest/googletest,
+ third_party/tensorpipe/third_party/googletest,
+ third_party/tensorpipe/third_party/googletest/googletest
+ For details, see the files concatenated below: third_party/fbgemm/third_party/googletest/LICENSE,
+ third_party/fbgemm/third_party/googletest/googletest/LICENSE,
+ third_party/googletest/LICENSE,
+ third_party/kineto/libkineto/third_party/dynolog/third_party/googletest/LICENSE,
+ third_party/kineto/libkineto/third_party/googletest/LICENSE,
+ third_party/kineto/libkineto/third_party/googletest/googletest/LICENSE,
+ third_party/opentelemetry-cpp/third_party/googletest/LICENSE,
+ third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/LICENSE,
+ third_party/protobuf/third_party/googletest/LICENSE,
+ third_party/protobuf/third_party/googletest/googletest/LICENSE,
+ third_party/tensorpipe/third_party/googletest/LICENSE,
+ third_party/tensorpipe/third_party/googletest/googletest/LICENSE
+
+Name: gtest
+License: BSD-3-Clause
+Files: third_party/ideep/mkl-dnn/tests/gtests/gtest
+ For details, see the files concatenated below: third_party/ideep/mkl-dnn/tests/gtests/gtest/LICENSE
+
+Name: hipify_torch
+License: MIT
+Files: third_party/fbgemm/third_party/hipify_torch
+ For details, see the files concatenated below: third_party/fbgemm/third_party/hipify_torch/LICENSE.txt
+
+Name: hungarian
+License: Permissive (free to use)
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/hungarian
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/hungarian/LICENSE.txt
+
+Name: ideep
+License: MIT
+Files: third_party/ideep
+ For details, see the files concatenated below: third_party/ideep/LICENSE
+
+Name: irrlicht
+License: MIT
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/irrlicht
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/irrlicht/LICENSE.txt
+
+Name: kineto
+License: BSD-3-Clause
+Files: third_party/kineto
+ For details, see the files concatenated below: third_party/kineto/LICENSE
+
+Name: libnop
+License: Apache-2.0
+Files: third_party/tensorpipe/third_party/libnop
+ For details, see the files concatenated below: third_party/tensorpipe/third_party/libnop/LICENSE
+
+Name: libstemmer
+License: BSD-3-Clause
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/libstemmer
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/libstemmer/LICENSE
+
+Name: libuv
+License: MIT
+Files: third_party/tensorpipe/third_party/libuv
+ For details, see the files concatenated below: third_party/tensorpipe/third_party/libuv/LICENSE
+
+Name: mimalloc
+License: MIT
+Files: third_party/mimalloc
+ For details, see the files concatenated below: third_party/mimalloc/LICENSE
+
+Name: miniz-2.1.0
+License: MIT
+Files: third_party/miniz-2.1.0
+ For details, see the files concatenated below: third_party/miniz-2.1.0/LICENSE
+
+Name: mkl-dnn
+License: Apache-2.0
+Files: third_party/ideep/mkl-dnn
+ For details, see the files concatenated below: third_party/ideep/mkl-dnn/LICENSE
+
+Name: ms-gsl
+License: MIT
+Files: third_party/opentelemetry-cpp/third_party/ms-gsl
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/ms-gsl/LICENSE
+
+Name: nccl
+License: BSD-3-Clause
+Files: third_party/nccl/nccl
+ For details, see the files concatenated below: third_party/nccl/nccl/LICENSE.txt
+
+Name: onnx
+License: Apache-2.0
+Files: third_party/onnx
+ For details, see the files concatenated below: third_party/onnx/LICENSE
+
+Name: opentelemetry-cpp
+License: Apache-2.0
+Files: third_party/opentelemetry-cpp
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/LICENSE
+
+Name: opentelemetry-proto
+License: Apache-2.0
+Files: third_party/opentelemetry-cpp/third_party/opentelemetry-proto
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/opentelemetry-proto/LICENSE
+
+Name: opentracing-cpp
+License: Apache-2.0
+Files: third_party/opentelemetry-cpp/third_party/opentracing-cpp
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/opentracing-cpp/LICENSE
+
+Name: pdcurses
+License: Public Domain for core
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/pdcurses
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/pdcurses/LICENSE
+
+Name: pfs
+License: Apache-2.0
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/pfs
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/pfs/LICENSE
+
+Name: physac
+License: MIT
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/physac
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/physac/LICENSE
+
+Name: pqp
+License: Apache-2.0
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/pqp
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/pqp/LICENSE
+
+Name: prometheus-cpp
+License: MIT
+Files: third_party/opentelemetry-cpp/third_party/prometheus-cpp
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/prometheus-cpp/LICENSE
+
+Name: protobuf
+License: BSD-3-Clause
+Files: third_party/protobuf
+ For details, see the files concatenated below: third_party/protobuf/LICENSE
+
+Name: psimd
+License: MIT
+Files: third_party/psimd
+ For details, see the files concatenated below: third_party/psimd/LICENSE
+
+Name: pthreadpool
+License: BSD-2-Clause
+Files: third_party/pthreadpool
+ For details, see the files concatenated below: third_party/pthreadpool/LICENSE
+
+Name: pybind11
+License: BSD-3-Clause
+Files: third_party/onnx/third_party/pybind11,
+ third_party/pybind11,
+ third_party/tensorpipe/third_party/pybind11
+ For details, see the files concatenated below: third_party/onnx/third_party/pybind11/LICENSE,
+ third_party/pybind11/LICENSE,
+ third_party/tensorpipe/third_party/pybind11/LICENSE
+
+Name: python
+License: Apache-2.0 with exception
+Files: third_party/NVTX/python
+ For details, see the files concatenated below: third_party/NVTX/python/LICENSE.txt
+
+Name: python
+License: BSD-3-Clause
+Files: third_party/cutlass/python
+ For details, see the files concatenated below: third_party/cutlass/python/LICENSE.txt
+
+Name: python-peachpy
+License: BSD-2-Clause
+Files: third_party/python-peachpy
+ For details, see the files concatenated below: third_party/python-peachpy/LICENSE.rst
+
+Name: sigslot
+License: Public Domain
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/sigslot
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/sigslot/LICENSE
+
+Name: sleef
+License: BSL-1.0
+Files: third_party/sleef
+ For details, see the files concatenated below: third_party/sleef/LICENSE.txt
+
+Name: swift
+License: Apache-2.0
+Files: third_party/flatbuffers/swift
+ For details, see the files concatenated below: third_party/flatbuffers/swift/LICENSE
+
+Name: tb_plugin
+License: BSD-3-Clause
+Files: third_party/kineto/tb_plugin
+ For details, see the files concatenated below: third_party/kineto/tb_plugin/LICENSE
+
+Name: tensorflow-common
+License: MIT
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/tensorflow-common
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/tensorflow-common/LICENSE.txt
+
+Name: tensorpipe
+License: BSD-3-Clause
+Files: third_party/tensorpipe
+ For details, see the files concatenated below: third_party/tensorpipe/LICENSE.txt
+
+Name: test
+License: MIT with exception
+Files: third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/test
+ For details, see the files concatenated below: third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/test/LICENSE
+
+Name: variant
+License: BSD-3-Clause
+Files: third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/variant
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/variant/LICENSE
+
+Name: vcpkg
+License: MIT
+Files: third_party/opentelemetry-cpp/tools/vcpkg
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/LICENSE.txt
+
+Name: vulkan
+License: Apache-2.0 with exception
+Files: third_party/opentelemetry-cpp/tools/vcpkg/ports/vulkan
+ For details, see the files concatenated below: third_party/opentelemetry-cpp/tools/vcpkg/ports/vulkan/LICENSE.txt
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/LICENSE
+-------------------------------------------------------------------------
+Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+third_party/FP16/LICENSE
+------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017 Facebook Inc.
+Copyright (c) 2017 Georgia Institute of Technology
+Copyright 2019 Google LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+third_party/FXdiv/LICENSE
+-------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017 Facebook Inc.
+Copyright (c) 2016-2017 Marat Dukhan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+third_party/NNPACK/LICENSE
+--------------------------
+Copyright (c) 2017 Facebook Inc.
+Copyright (c) 2015-2017, Georgia Institute of Technology
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/NVTX/LICENSE.txt
+----------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+--- LLVM Exceptions to the Apache 2.0 License ----
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into an Object form of such source code, you
+may redistribute such embedded portions in such Object form without complying
+with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
+
+In addition, if you combine or link compiled forms of this Software with
+software that is licensed under the GPLv2 ("Combined Software") and if a
+court of competent jurisdiction determines that the patent provision (Section
+3), the indemnity provision (Section 9) or other Section of the License
+conflicts with the conditions of the GPLv2, you may retroactively and
+prospectively choose to deem waived or otherwise exclude such Section(s) of
+the License, but only in their entirety and only with respect to the Combined
+Software.
+
+
+
+third_party/VulkanMemoryAllocator/LICENSE.txt
+---------------------------------------------
+Copyright (c) 2017-2022 Advanced Micro Devices, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+third_party/XNNPACK/LICENSE
+---------------------------
+BSD License
+
+For XNNPACK software
+
+Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
+Copyright 2019 Google LLC
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/benchmark/LICENSE
+-----------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/onnx/third_party/benchmark/LICENSE
+----------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/third_party/benchmark/LICENSE
+-----------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/protobuf/third_party/benchmark/LICENSE
+--------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/boost-vcpkg-helpers/LICENSE.txt
+-------------------------------------------------------------------------------
+Copyright (c) Microsoft Corporation
+
+All rights reserved.
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/examples/rest/cJSON/LICENSE
+------------------------------------------------------------------------------------------------------
+Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+
+third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/catch2/LICENSE.txt
+----------------------------------------------------------------------------------------------------------
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+
+third_party/cpuinfo/deps/clog/LICENSE
+-------------------------------------
+Copyright (C) 2018 Marat Dukhan
+Copyright (c) 2017-2018 Facebook Inc.
+Copyright (c) 2017 Georgia Institute of Technology
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/cpuinfo/deps/clog/LICENSE
+--------------------------------------------------------
+Copyright (C) 2018 Marat Dukhan
+Copyright (c) 2017-2018 Facebook Inc.
+Copyright (c) 2017 Georgia Institute of Technology
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/DCGM/testing/python3/libs_3rdparty/colorama/LICENSE.txt
+--------------------------------------------------------------------------------------------------------------------
+Copyright (c) 2010 Jonathan Hartley
+
+Released under the New BSD license (reproduced below), or alternatively you may
+use this software under any OSI approved open source license such as those at
+http://opensource.org/licenses/alphabetical
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name(s) of the copyright holders, nor those of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+third_party/cpp-httplib/LICENSE
+-------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017 yhirose
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/json/third_party/cpplint/LICENSE
+---------------------------------------------------------------------------------------------
+cpplint.py and its corresponding unit tests are Copyright (C) 2009 Google Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/nlohmann/tools/cpplint/LICENSE
+------------------------------------------
+cpplint.py and its corresponding unit tests are Copyright (C) 2009 Google Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/LICENSE
+------------------------------------------------------------------------
+This license applies to everything except the contents of the "test"
+directory and its subdirectories.
+
+MIT License
+
+Copyright (c) 2017-2021 Huu Nguyen
+Copyright (c) 2022 libcpr and many other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+third_party/cpuinfo/LICENSE
+---------------------------
+Copyright (c) 2019 Google LLC
+Copyright (c) 2017-2018 Facebook Inc.
+Copyright (C) 2012-2017 Georgia Institute of Technology
+Copyright (C) 2010-2012 Marat Dukhan
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/cpuinfo/LICENSE
+----------------------------------------------
+Copyright (c) 2019 Google LLC
+Copyright (c) 2017-2018 Facebook Inc.
+Copyright (C) 2012-2017 Georgia Institute of Technology
+Copyright (C) 2010-2012 Marat Dukhan
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/cudnn_frontend/LICENSE.txt
+--------------------------------------
+/*
+ * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+third_party/cutlass/LICENSE.txt
+-------------------------------
+Copyright (c) 2017 - 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+SPDX-License-Identifier: BSD-3-Clause
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/cutlass/LICENSE.txt
+--------------------------------------------------
+Copyright (c) 2017 - 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+SPDX-License-Identifier: BSD-3-Clause
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/flatbuffers/dart/LICENSE
+------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2014 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/json/test/thirdparty/doctest/LICENSE.txt
+-----------------------------------------------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Viktor Kirilov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/nlohmann/tests/thirdparty/doctest/LICENSE.txt
+---------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Viktor Kirilov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.5.2/LICENSE.txt
+--------------------------------------------------------------------------------------------------------------------
+===============
+Duktape license
+===============
+
+(http://opensource.org/licenses/MIT)
+
+Copyright (c) 2013-2016 by Duktape authors (see AUTHORS.rst)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/civetweb/src/third_party/duktape-1.8.0/LICENSE.txt
+--------------------------------------------------------------------------------------------------------------------
+===============
+Duktape license
+===============
+
+(http://opensource.org/licenses/MIT)
+
+Copyright (c) 2013-2017 by Duktape authors (see AUTHORS.rst)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+third_party/kineto/libkineto/third_party/dynolog/LICENSE
+--------------------------------------------------------
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/eigen/COPYING.BSD
+-----------------------------
+/*
+ Copyright (c) 2011, Intel Corporation. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of Intel Corporation nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+third_party/opentelemetry-cpp/exporters/etw/include/opentelemetry/exporters/etw/LICENSE
+---------------------------------------------------------------------------------------
+TraceLogging Dynamic for Windows
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/expected/LICENSE
+--------------------------------------------------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2015 Martin Moene
+Copyright (c) 2015 Microsoft Corporation. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+third_party/fbgemm/LICENSE
+--------------------------
+BSD License
+
+For FBGEMM software
+
+Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/ffnvcodec/LICENSE.txt
+---------------------------------------------------------------------
+GNU LESSER GENERAL PUBLIC LICENSE
+Version 2.1, February 1999
+
+Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+Preamble
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
+
+This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
+
+When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
+
+To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
+
+For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
+
+We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
+
+To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
+
+Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
+
+Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
+
+When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
+
+We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
+
+For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
+
+In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
+
+Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
+
+The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
+
+A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
+
+The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
+
+"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
+
+Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
+
+1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
+
+You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+
+a) The modified work must itself be a software library.
+b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
+c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
+d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
+(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
+
+Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
+
+This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
+
+4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
+
+If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
+
+5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
+
+However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
+
+When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
+
+If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
+
+Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
+
+6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
+
+You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
+
+a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
+b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
+c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
+d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
+e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
+For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+
+It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
+
+7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
+
+a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
+b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
+8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+
+9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
+
+10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
+
+11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+
+12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+
+13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
+
+14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
+How to Apply These Terms to Your New Libraries
+If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
+
+To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+one line to give the library's name and an idea of what it does.
+Copyright (C) year name of author
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
+
+Yoyodyne, Inc., hereby disclaims all copyright interest in
+the library `Frob' (a library for tweaking knobs) written
+by James Random Hacker.
+
+signature of Ty Coon, 1 April 1990
+Ty Coon, President of Vice
+That's all there is to it!
+
+third_party/flatbuffers/LICENSE
+-------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/fmt/LICENSE
+-----------------------
+Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/fmt/LICENSE.rst
+----------------------------------------------------------------------------
+Copyright (c) 2012 - present, Victor Zverovich
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
+
+
+third_party/kineto/libkineto/third_party/fmt/LICENSE
+----------------------------------------------------
+Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
+
+
+third_party/gemmlowp/gemmlowp/LICENSE
+-------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/fbgemm/third_party/googletest/googlemock/scripts/generator/LICENSE
+------------------------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2007] Neal Norwitz
+ Portions Copyright [2007] Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/googletest/googlemock/scripts/generator/LICENSE
+-----------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2007] Neal Norwitz
+ Portions Copyright [2007] Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/kineto/libkineto/third_party/googletest/googlemock/scripts/generator/LICENSE
+----------------------------------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2007] Neal Norwitz
+ Portions Copyright [2007] Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/googlemock/scripts/generator/LICENSE
+-----------------------------------------------------------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2007] Neal Norwitz
+ Portions Copyright [2007] Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/protobuf/third_party/googletest/googlemock/scripts/generator/LICENSE
+--------------------------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2007] Neal Norwitz
+ Portions Copyright [2007] Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/tensorpipe/third_party/googletest/googlemock/scripts/generator/LICENSE
+----------------------------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2007] Neal Norwitz
+ Portions Copyright [2007] Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/gettimeofday/LICENSE
+--------------------------------------------------------------------
+/*
+ * Copied from PostgreSQL source:
+ * http://doxygen.postgresql.org/gettimeofday_8c_source.html
+ *
+ */
+
+/*
+ * gettimeofday.c
+ * Win32 gettimeofday() replacement
+ *
+ * src/port/gettimeofday.c
+ *
+ * Copyright (c) 2003 SRA, Inc.
+ * Copyright (c) 2003 SKC, Inc.
+ *
+ * Permission to use, copy, modify, and distribute this software and
+ * its documentation for any purpose, without fee, and without a
+ * written agreement is hereby granted, provided that the above
+ * copyright notice and this paragraph and the following two
+ * paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT,
+ * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS
+ * IS" BASIS, AND THE AUTHOR HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE,
+ * SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+
+
+third_party/gloo/LICENSE
+------------------------
+BSD License
+
+For Gloo software
+
+Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/googletest/googlemock/LICENSE
+------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/googletest/googlemock/LICENSE
+----------------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/protobuf/third_party/googletest/googlemock/LICENSE
+--------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/tensorpipe/third_party/googletest/googlemock/LICENSE
+----------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/googletest/LICENSE
+-------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/googletest/googletest/LICENSE
+------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/googletest/LICENSE
+------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/googletest/LICENSE
+-------------------------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/googletest/LICENSE
+-----------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/googletest/googletest/LICENSE
+----------------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/opentelemetry-cpp/third_party/googletest/LICENSE
+------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/opentelemetry-cpp/third_party/prometheus-cpp/3rdparty/googletest/LICENSE
+------------------------------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/protobuf/third_party/googletest/LICENSE
+---------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/protobuf/third_party/googletest/googletest/LICENSE
+--------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/tensorpipe/third_party/googletest/LICENSE
+-----------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/tensorpipe/third_party/googletest/googletest/LICENSE
+----------------------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/ideep/mkl-dnn/tests/gtests/gtest/LICENSE
+----------------------------------------------------
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/fbgemm/third_party/hipify_torch/LICENSE.txt
+-------------------------------------------------------
+MIT License
+
+Copyright (c) 2017 AMD Compute Libraries
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/hungarian/LICENSE.txt
+---------------------------------------------------------------------
+/********************************************************************
+ ********************************************************************
+ **
+ ** libhungarian by Cyrill Stachniss, 2004
+ **
+ **
+ ** Solving the Minimum Assignment Problem using the
+ ** Hungarian Method.
+ **
+ ** ** This file may be freely copied and distributed! **
+ **
+ ** Parts of the used code was originally provided by the
+ ** "Stanford GraphGase", but I made changes to this code.
+ ** As asked by the copyright node of the "Stanford GraphGase",
+ ** I hereby proclaim that this file are *NOT* part of the
+ ** "Stanford GraphGase" distrubition!
+ **
+ ** This file is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied
+ ** warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ ** PURPOSE.
+ **
+ ********************************************************************
+ ********************************************************************/
+
+
+third_party/ideep/LICENSE
+-------------------------
+Copyright (c) 2018 Intel Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/irrlicht/LICENSE.txt
+--------------------------------------------------------------------
+The Irrlicht Engine License
+===========================
+
+Copyright (C) 2002-2015 Nikolaus Gebhardt
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgement in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be clearly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+third_party/kineto/LICENSE
+--------------------------
+BSD License
+
+For Kineto software
+
+Copyright (c) Meta Platforms, Inc. and affiliates.
+
+All contributions by Microsoft:
+Copyright (c) Microsoft Corporation. (The Azure AI Platform team)
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Meta nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/tensorpipe/third_party/libnop/LICENSE
+-------------------------------------------------
+Copyright 2017 The Native Object Protocols Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/libstemmer/LICENSE
+------------------------------------------------------------------
+Snowball - License
+Except where explicitly noted, all the software given out on this Snowball site is covered by the 3-clause BSD License:
+
+Copyright (c) 2001, Dr Martin Porter,
+Copyright (c) 2002, Richard Boulton.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+Essentially, all this means is that you can do what you like with the code, except claim another Copyright for it, or claim that it is issued under a different license. The software is also issued without warranties, which means that if anyone suffers through its use, they cannot come back and sue you. You also have to alert anyone to whom you give the Snowball software to the fact that it is covered by the BSD license.
+
+We have not bothered to insert the licensing arrangement into the text of the Snowball software.
+
+
+third_party/tensorpipe/third_party/libuv/LICENSE
+------------------------------------------------
+libuv is licensed for use as follows:
+
+====
+Copyright (c) 2015-present libuv project contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+====
+
+This license applies to parts of libuv originating from the
+https://github.com/joyent/libuv repository:
+
+====
+
+Copyright Joyent, Inc. and other Node contributors. All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+
+====
+
+This license applies to all parts of libuv that are not externally
+maintained libraries.
+
+The externally maintained libraries used by libuv are:
+
+ - tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license.
+
+ - inet_pton and inet_ntop implementations, contained in src/inet.c, are
+ copyright the Internet Systems Consortium, Inc., and licensed under the ISC
+ license.
+
+ - stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three
+ clause BSD license.
+
+ - pthread-fixes.c, copyright Google Inc. and Sony Mobile Communications AB.
+ Three clause BSD license.
+
+ - android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design
+ Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement
+ nยฐ 289016). Three clause BSD license.
+
+
+third_party/mimalloc/LICENSE
+----------------------------
+MIT License
+
+Copyright (c) 2018-2021 Microsoft Corporation, Daan Leijen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/miniz-2.1.0/LICENSE
+-------------------------------
+Copyright 2013-2014 RAD Game Tools and Valve Software
+Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
+
+All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+third_party/ideep/mkl-dnn/LICENSE
+---------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ ============================================================================
+
+ Copyright 2016-2023 Intel Corporation
+ Copyright 2018 YANDEX LLC
+ Copyright 2019-2023 FUJITSU LIMITED
+ Copyright 2020-2023 Arm Ltd. and affiliates
+ Copyright 2020-2022 Codeplay Software Limited
+ Copyright 2021 Alanna Tempest
+ Copyright 2022-2023 IBM Corporation
+ Copyright 2023 KNS Group LLC (YADRO)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ This distribution includes third party software ("third party programs").
+ This third party software, even if included with the distribution of
+ the Intel software, may be governed by separate license terms, including
+ without limitation, third party license terms, other Intel software license
+ terms, and open source software license terms. These separate license terms
+ govern your use of the third party programs as set forth in the
+ "THIRD-PARTY-PROGRAMS" file.
+
+
+third_party/opentelemetry-cpp/third_party/ms-gsl/LICENSE
+--------------------------------------------------------
+Copyright (c) 2015 Microsoft Corporation. All rights reserved.
+
+This code is licensed under the MIT License (MIT).
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+third_party/nccl/nccl/LICENSE.txt
+---------------------------------
+
+ Copyright (c) 2015-2020, NVIDIA CORPORATION. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of NVIDIA CORPORATION, Lawrence Berkeley National
+ Laboratory, the U.S. Department of Energy, nor the names of their
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ The U.S. Department of Energy funded the development of this software
+ under subcontract 7078610 with Lawrence Berkeley National Laboratory.
+
+
+This code also includes files from the NVIDIA Tools Extension SDK project.
+
+See:
+
+ https://github.com/NVIDIA/NVTX
+
+for more information and license details.
+
+
+third_party/onnx/LICENSE
+------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/LICENSE
+-------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/third_party/opentelemetry-proto/LICENSE
+---------------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/third_party/opentracing-cpp/LICENSE
+-----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright The OpenTracing Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/pdcurses/LICENSE
+----------------------------------------------------------------
+The core package is in the public domain, but small portions of PDCurses are subject to copyright under various licenses.
+
+The win32 files are released to the public domain.
+
+If you use PDCurses in an application, an acknowledgement would be appreciated, but is not mandatory. If you make corrections or enhancements to PDCurses, please forward them to the current maintainer for the benefit of other users.
+
+This software is provided AS IS with NO WARRANTY whatsoever.
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/pfs/LICENSE
+------------------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2020-present Daniel Trugman
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/physac/LICENSE
+--------------------------------------------------------------
+MIT License
+
+Copyright (c) 2022 Vรญctor Fisac
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/pqp/LICENSE
+-----------------------------------------------------------
+Copyright 1999 University of North Carolina at Chapel Hill.
+All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for educational, research, and non-profit purposes, without fee,
+and without a written agreement is hereby granted, provided that the above
+copyright notice and the following three paragraphs appear in all copies.
+
+IN NO EVENT SHALL THE UNIVERSITY OF NORTH CAROLINA AT CHAPEL HILL BE LIABLE TO
+ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
+INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+DOCUMENTATION, EVEN IF THE UNIVERSITY OF NORTH CAROLINA AT CHAPEL HILL HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+THE UNIVERSITY OF NORTH CAROLINA AT CHAPEL HILL SPECIFICALLY DISCLAIMS ANY
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED
+HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA AT
+CHAPEL HILL HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
+ENHANCEMENTS, OR MODIFICATIONS.
+
+The authors may be contacted via:
+
+US Mail: Eric Larsen, Stefan Gottschalk
+ Department of Computer Science
+ Sitterson Hall, CB #3175
+ University of North Carolina
+ Chapel Hill, NC 27599-3175
+
+Phone: (919) 962-1749
+
+Email: geom@cs.unc.edu
+
+third_party/opentelemetry-cpp/third_party/prometheus-cpp/LICENSE
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2016-2021 Jupp Mueller
+Copyright (c) 2017-2022 Gregor Jasny
+
+And many contributors, see
+https://github.com/jupp0r/prometheus-cpp/graphs/contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/protobuf/LICENSE
+----------------------------
+Copyright 2008 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it. This code is not
+standalone and requires a support library to be linked with it. This
+support library is itself covered by the above license.
+
+
+third_party/psimd/LICENSE
+-------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017 Facebook Inc.
+Copyright (c) 2014-2017 Georgia Institute of Technology
+Copyright 2019 Google LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+third_party/pthreadpool/LICENSE
+-------------------------------
+Copyright 2019 Google LLC
+Copyright (c) 2017 Facebook Inc.
+Copyright (c) 2015-2017 Georgia Institute of Technology
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+third_party/onnx/third_party/pybind11/LICENSE
+---------------------------------------------
+Copyright (c) 2016 Wenzel Jakob , All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of
+external contributions to this project including patches, pull requests, etc.
+
+
+third_party/pybind11/LICENSE
+----------------------------
+Copyright (c) 2016 Wenzel Jakob , All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of
+external contributions to this project including patches, pull requests, etc.
+
+
+third_party/tensorpipe/third_party/pybind11/LICENSE
+---------------------------------------------------
+Copyright (c) 2016 Wenzel Jakob , All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Please also refer to the file CONTRIBUTING.md, which clarifies licensing of
+external contributions to this project including patches, pull requests, etc.
+
+
+third_party/NVTX/python/LICENSE.txt
+-----------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+--- LLVM Exceptions to the Apache 2.0 License ----
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into an Object form of such source code, you
+may redistribute such embedded portions in such Object form without complying
+with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
+
+In addition, if you combine or link compiled forms of this Software with
+software that is licensed under the GPLv2 ("Combined Software") and if a
+court of competent jurisdiction determines that the patent provision (Section
+3), the indemnity provision (Section 9) or other Section of the License
+conflicts with the conditions of the GPLv2, you may retroactively and
+prospectively choose to deem waived or otherwise exclude such Section(s) of
+the License, but only in their entirety and only with respect to the Combined
+Software.
+
+
+
+third_party/cutlass/python/LICENSE.txt
+--------------------------------------
+Copyright (c) 2017 - 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+SPDX-License-Identifier: BSD-3-Clause
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/python-peachpy/LICENSE.rst
+--------------------------------------
+==============================
+PeachPy license (2-clause BSD)
+==============================
+
+Copyright (c) 2017, Facebook Inc.
+Copyright (c) 2013-2017, Georgia Institute of Technology
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/sigslot/LICENSE
+---------------------------------------------------------------
+License
+The sigslot library has been placed in the public domain. This means that you are free to use it however you like.
+
+The author takes no responsibility or liability of any kind for any use that you may make of this library.
+
+If you screw up, it's your fault.
+
+If the library screws up, you got it for free, so you should have tested it better - it's still your responsibility.
+
+third_party/sleef/LICENSE.txt
+-----------------------------
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+
+third_party/flatbuffers/swift/LICENSE
+-------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+third_party/kineto/tb_plugin/LICENSE
+------------------------------------
+BSD License
+
+For Kineto software
+
+Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
+
+All contributions by Microsoft:
+Copyright (c) Microsoft Corporation. (The Azure AI Platform team)
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/tensorflow-common/LICENSE.txt
+-----------------------------------------------------------------------------
+Copyright (c) Microsoft Corporation
+
+All rights reserved.
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+third_party/tensorpipe/LICENSE.txt
+----------------------------------
+BSD License
+
+For TensorPipe software
+
+Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Meta nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+third_party/kineto/libkineto/third_party/dynolog/third_party/cpr/test/LICENSE
+-----------------------------------------------------------------------------
+This license applies to everything inside this directory and all
+subdirectories.
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
+
+third_party/opentelemetry-cpp/third_party/opentracing-cpp/3rd_party/include/opentracing/variant/LICENSE
+-------------------------------------------------------------------------------------------------------
+Copyright (c) MapBox
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+- Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+- Neither the name "MapBox" nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+third_party/opentelemetry-cpp/tools/vcpkg/LICENSE.txt
+-----------------------------------------------------
+MIT License
+
+Copyright (c) Microsoft Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be included in all copies
+or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
+OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+third_party/opentelemetry-cpp/tools/vcpkg/ports/vulkan/LICENSE.txt
+------------------------------------------------------------------
+/*
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of this License; and
+You must cause any modified files to carry prominent notices stating that You changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+===============================================================================================================================================
+
+/Copyright (C) 2012 LunarG, Inc.
+//All rights reserved.
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions
+//are met:
+//
+// Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+//
+// Neither the name of LunarG Inc. nor the names of its
+// contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+//"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+//LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+//FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+//COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+//INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+//BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+//LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+//ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+//POSSIBILITY OF SUCH DAMAGE.
+
+===============================================================================================================================================
+
+#=============================================================================
+# Copyright 2007-2009 Kitware, Inc.
+# Copyright 2007-2008 Miguel A. Figueroa-Villanueva
+#
+# Distributed under the OSI-approved BSD License (the "License");
+# see accompanying file Copyright_cmake.txt for details.
+#
+# This software is distributed WITHOUT ANY WARRANTY; without even the
+# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the License for more information.
+#=============================================================================
+# (To distributed this file outside of CMake, substitute the full
+# License text for the above reference.)
+
+
+==============================================================================================================================================
+
+//
+// Copyright (C) 2015-2018 Google, Inc.
+// Copyright (C)
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+//
+// Neither the name of 3Dlabs Inc. Ltd. nor the names of its
+// contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+//
+
+==========================================================================================================================================
+
+Note: This license has also been called the "New BSD License" or "Modified BSD License". See also the 2-clause BSD License.
+Copyright
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+==========================================================================================================================================
+
+/*
+* xxHash - Fast Hash algorithm
+* Copyright (C) 2012-2016, Yann Collet
+*
+* BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
+*
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions are
+* met:
+*
+* * Redistributions of source code must retain the above copyright
+* notice, this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above
+* copyright notice, this list of conditions and the following disclaimer
+* in the documentation and/or other materials provided with the
+* distribution.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+* You can contact the author at :
+* - xxHash homepage: http://www.xxhash.com
+* - xxHash source repository : https://github.com/Cyan4973/xxHash
+*/
+
+
+===========================================================================================================================================
+
+# Copyright (C) 2018 Google, Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+#
+# Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+==========================================================================================================================================
+
+/* A Bison parser, made by GNU Bison 3.0.4. */
+
+/* Bison implementation for Yacc-like parsers in C
+Copyright (C) 1984, 1989-1990, 2000-2015 Free Software Foundation, Inc.
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+You should have received a copy of the GNU General Public License
+along with this program. If not, see . */
+
+/* As a special exception, you may create a larger work that contains
+part or all of the Bison parser skeleton and distribute that work
+under terms of your choice, so long as that work isn't itself a
+parser generator using the skeleton or a modified version thereof
+as a parser skeleton. Alternatively, if you modify or redistribute
+the parser skeleton itself, you may (at your option) remove this
+special exception, which will cause the skeleton and the resulting
+Bison output files to be licensed under the GNU General Public
+License without this special exception.
+This special exception was added by the Free Software Foundation in
+version 2.2 of Bison. */
+
+/* C LALR(1) parser skeleton written by Richard Stallman, by
+simplifying the original so-called "semantic" parser. */
+
+/* All symbols defined below should begin with yy or YY, to avoid
+infringing on user name space. This should be done even for local
+variables, as they might otherwise be expanded by user macros.
+There are some unavoidable exceptions within include files to
+define necessary library symbols; they are noted "INFRINGES ON
+USER NAME SPACE" below. */
+
+==============================================================================================================================================
+
+copyright : [
+Copyright (c) 2017 The Khronos Group Inc.,
+,
+Permission is hereby granted, free of charge, to any person obtaining a copy,
+of this software and/or associated documentation files (the \Materials\"),",
+to deal in the Materials without restriction, including without limitation,
+the rights to use, copy, modify, merge, publish, distribute, sublicense,,
+and/or sell copies of the Materials, and to permit persons to whom the,
+Materials are furnished to do so, subject to the following conditions:,
+,
+The above copyright notice and this permission notice shall be included in,
+all copies or substantial portions of the Materials.,
+,
+MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS,
+STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND,
+HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ ,
+,
+THE MATERIALS ARE PROVIDED \AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS",
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL,
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER,
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING,
+FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS,
+IN THE MATERIALS.
+
+=============================================================================================================================================
+
+CMake - Cross Platform Makefile Generator
+Copyright 2000-2009 Kitware, Inc., Insight Software Consortium
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+* Neither the names of Kitware, Inc., the Insight Software Consortium,
+nor the names of their contributors may be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------------------------------------------------------------------
+
+The above copyright and license notice applies to distributions of
+CMake in source and binary form. Some source files contain additional
+notices of original copyright by their contributors; see each source
+for details. Third-party software packages supplied with CMake under
+compatible licenses provide their own copyright notices documented in
+corresponding subdirectories.
+
+------------------------------------------------------------------------------
+
+CMake was initially developed by Kitware with the following sponsorship:
+
+* National Library of Medicine at the National Institutes of Health
+as part of the Insight Segmentation and Registration Toolkit (ITK).
+
+* US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel
+Visualization Initiative.
+
+* National Alliance for Medical Image Computing (NAMIC) is funded by the
+National Institutes of Health through the NIH Roadmap for Medical Research,
+Grant U54 EB005149.
+
+* Kitware, Inc.
+
+========================================================================================================================================
+
+The authors of this software are Rob Pike and Ken Thompson.
+* Copyright (c) 2002 by Lucent Technologies.
+* Permission to use, copy, modify, and distribute this software for any
+* purpose without fee is hereby granted, provided that this entire notice
+* is included in all copies of any software which is or includes a copy
+* or modification of this software and in all copies of the supporting
+* documentation for such software.
+* THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
+* WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR LUCENT TECHNOLOGIES MAKE ANY
+* REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY
+* OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
+
+
+========================================================================================================================================
+
+Copyright (c) 2015-2018 Baldur Karlsson
+
+Copyright (c) 2014 Crytek
+
+Copyright (c) 1998-2018 Third party code and tools
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+=========================================================================================================================================
+
+/*
+Copyright (c) 2009 Dave Gamble
+Copyright (c) 2015-2016 The Khronos Group Inc.
+Copyright (c) 2015-2016 Valve Corporation
+Copyright (c) 2015-2016 LunarG, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+===========================================================================================================================================
+
+Copyright (c) 2005 - 2017 G-Truc Creation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+
+
+==========================================================================================================================================
+
+/*
+The JsonCpp library's source code, including accompanying documentation,
+tests and demonstration applications, are licensed under the following
+conditions...
+The author (Baptiste Lepilleur) explicitly disclaims copyright in all
+jurisdictions which recognize such a disclaimer. In such jurisdictions,
+this software is released into the Public Domain.
+In jurisdictions which do not recognize Public Domain property (e.g. Germany as of
+2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is
+released under the terms of the MIT License (see below).
+In jurisdictions which recognize Public Domain property, the user of this
+software may choose to accept it either as 1) Public Domain, 2) under the
+conditions of the MIT License (see below), or 3) under the terms of dual
+Public Domain/MIT License conditions described here, as they choose.
+The MIT License is about as close to Public Domain as a license can get, and is
+described in clear, concise terms at:
+http://en.wikipedia.org/wiki/MIT_License
+
+The full text of the MIT License follows:
+
+Copyright (c) 2007-2010 Baptiste Lepilleur
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+==========================================================================================================================================
+
+/**
+* `murmurhash.h' - murmurhash
+*
+* copyright (c) 2014 joseph werle
+* Copyright (c) 2015-2016 The Khronos Group Inc.
+* Copyright (c) 2015-2016 Valve Corporation
+* Copyright (c) 2015-2016 LunarG, Inc.
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and/or associated documentation files (the "Materials"), to
+* deal in the Materials without restriction, including without limitation the
+* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+* sell copies of the Materials, and to permit persons to whom the Materials are
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice(s) and this permission notice shall be included in
+* all copies or substantial portions of the Materials.
+*
+* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+*
+* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE
+* USE OR OTHER DEALINGS IN THE MATERIALS.
+*/
+
+=========================================================================================================================================
+
+Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
+This basically means: do what you want with it.
+
+=========================================================================================================================================
+
+///////////////////////////////////////////////////////////////////////////////////
+/// OpenGL Mathematics (glm.g-truc.net)
+///
+/// Copyright (c) 2005 - 2014 G-Truc Creation (www.g-truc.net)
+/// Permission is hereby granted, free of charge, to any person obtaining a copy
+/// of this software and associated documentation files (the "Software"), to deal
+/// in the Software without restriction, including without limitation the rights
+/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+/// copies of the Software, and to permit persons to whom the Software is
+/// furnished to do so, subject to the following conditions:
+///
+/// The above copyright notice and this permission notice shall be included in
+/// all copies or substantial portions of the Software.
+///
+/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+/// THE SOFTWARE.
+///
+/// @ref core
+/// @file glm/common.hpp
+/// @date 2013-12-24 / 2013-12-24
+/// @author Christophe Riccio
+///////////////////////////////////////////////////////////////////////////////////
+
+
+==========================================================================================================================================
+
+// LICENSE
+//
+// This software is in the public domain. Where that dedication is not
+// recognized, you are granted a perpetual, irrevocable license to copy,
+// distribute, and modify this file as you see fit.
+//
+
+==========================================================================================================================================
+
+Simple DirectMedia Layer
+Copyright (C) 1997-2018 Sam Lantinga
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+=========================================================================================================================================
+
+/****************************************************************************\
+Copyright (c) 2002, NVIDIA Corporation.
+
+NVIDIA Corporation("NVIDIA") supplies this software to you in
+consideration of your agreement to the following terms, and your use,
+installation, modification or redistribution of this NVIDIA software
+constitutes acceptance of these terms. If you do not agree with these
+terms, please do not use, install, modify or redistribute this NVIDIA
+software.
+
+In consideration of your agreement to abide by the following terms, and
+subject to these terms, NVIDIA grants you a personal, non-exclusive
+license, under NVIDIA's copyrights in this original NVIDIA software (the
+NVIDIA Software), to use, reproduce, modify and redistribute the
+NVIDIA Software, with or without modifications, in source and/or binary
+forms; provided that if you redistribute the NVIDIA Software, you must
+retain the copyright notice of NVIDIA, this notice and the following
+text and disclaimers in all such redistributions of the NVIDIA Software.
+Neither the name, trademarks, service marks nor logos of NVIDIA
+Corporation may be used to endorse or promote products derived from the
+NVIDIA Software without specific prior written permission from NVIDIA.
+Except as expressly stated in this notice, no other rights or licenses
+express or implied, are granted by NVIDIA herein, including but not
+limited to any patent rights that may be infringed by your derivative
+works or by other works in which the NVIDIA Software may be
+incorporated. No hardware is licensed hereunder.
+
+THE NVIDIA SOFTWARE IS BEING PROVIDED ON AN "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
+INCLUDING WITHOUT LIMITATION, WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
+ITS USE AND OPERATION EITHER ALONE OR IN COMBINATION WITH OTHER
+PRODUCTS.
+
+IN NO EVENT SHALL NVIDIA BE LIABLE FOR ANY SPECIAL, INDIRECT,
+INCIDENTAL, EXEMPLARY, CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, LOST PROFITS; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) OR ARISING IN ANY WAY
+OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE
+NVIDIA SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT,
+TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF
+NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+\****************************************************************************/
+
+==================================================================================================================================================
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+
+==================================================================================================================================================
+
+GNU LESSER GENERAL PUBLIC LICENSE
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below.
+
+0. Additional Definitions.
+
+As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License.
+
+"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below.
+
+An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library.
+
+A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version".
+
+The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version.
+
+The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work.
+
+1. Exception to Section 3 of the GNU GPL.
+
+You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL.
+
+2. Conveying Modified Versions.
+
+If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version:
+
+a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or
+b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy.
+3. Object Code Incorporating Material from Library Header Files.
+
+The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following:
+
+a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License.
+b) Accompany the object code with a copy of the GNU GPL and this license document.
+4. Combined Works.
+
+You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following:
+
+a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License.
+b) Accompany the Combined Work with a copy of the GNU GPL and this license document.
+c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document.
+d) Do one of the following:
+0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
+1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version.
+e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.)
+5. Combined Libraries.
+
+You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following:
+
+a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License.
+b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
+6. Revised Versions of the GNU Lesser General Public License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation.
+
+If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.
+```
+
+
+## torch_tensorrt (v2.4.0+cu118) - [BSD License](https://pytorch.org/tensorrt)
+
+```
+Copyright (c) 2020-present, NVIDIA CORPORATION. All rights reserved.
+Copyright (c) Meta Platforms, Inc. and affiliates.
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## torchvision (v0.20.1+cu118) - [BSD License](https://github.com/pytorch/vision)
+
+```
+BSD 3-Clause License
+
+Copyright (c) Soumith Chintala 2016,
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## tornado (v6.4.2) - [Apache License 2.0](http://www.tornadoweb.org/)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## tqdm (v4.67.1) - [MIT License; Mozilla Public License 2.0 (MPL 2.0)](https://tqdm.github.io)
+
+```
+`tqdm` is a product of collaborative work.
+Unless otherwise stated, all authors (see commit logs) retain copyright
+for their respective work, and release the work under the MIT licence
+(text below).
+
+Exceptions or notable authors are listed below
+in reverse chronological order:
+
+* files: *
+ MPL-2.0 2015-2024 (c) Casper da Costa-Luis
+ [casperdcl](https://github.com/casperdcl).
+* files: tqdm/_tqdm.py
+ MIT 2016 (c) [PR #96] on behalf of Google Inc.
+* files: tqdm/_tqdm.py README.rst .gitignore
+ MIT 2013 (c) Noam Yorav-Raphael, original author.
+
+[PR #96]: https://github.com/tqdm/tqdm/pull/96
+
+
+Mozilla Public Licence (MPL) v. 2.0 - Exhibit A
+-----------------------------------------------
+
+This Source Code Form is subject to the terms of the
+Mozilla Public License, v. 2.0.
+If a copy of the MPL was not distributed with this project,
+You can obtain one at https://mozilla.org/MPL/2.0/.
+
+
+MIT License (MIT)
+-----------------
+
+Copyright (c) 2013 noamraph
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## traitlets (v5.14.3) - [BSD License](https://github.com/ipython/traitlets)
+
+```
+BSD 3-Clause License
+
+- Copyright (c) 2001-, IPython Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## transformers (v4.50.3) - [Apache License 2.0](https://github.com/huggingface/transformers)
+
+```
+Copyright 2018- The Hugging Face team. All rights reserved.
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## trimesh (v4.6.6) - [MIT License](https://github.com/mikedh/trimesh)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2023 Michael Dawson-Haggerty
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## typing-inspect (v0.9.0) - [MIT License](https://github.com/ilevkivskyi/typing_inspect)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2017-2019 Ivan Levkivskyi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+
+## typing-inspection (v0.4.0) - [MIT License](https://github.com/pydantic/typing-inspection)
+
+```
+MIT License
+
+Copyright (c) 2025 Victorien
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## typing_extensions (v4.12.2) - [Python Software Foundation License](https://github.com/python/typing_extensions)
+
+```
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations, which became
+Zope Corporation. In 2001, the Python Software Foundation (PSF, see
+https://www.python.org/psf/) was formed, a non-profit organization
+created specifically to own Python-related Intellectual Property.
+Zope Corporation was a sponsoring member of the PSF.
+
+All Python releases are Open Source (see https://opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+Python software and documentation are licensed under the
+Python Software Foundation License Version 2.
+
+Starting with Python 3.8.6, examples, recipes, and other code in
+the documentation are dual licensed under the PSF License Version 2
+and the Zero-Clause BSD license.
+
+Some software incorporated into Python is under different licenses.
+The licenses are listed with code falling under that license.
+
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
+All Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
+----------------------------------------------------------------------
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+```
+
+
+## tzdata (v2025.2) - [Apache License 2.0](https://github.com/python/tzdata)
+
+```
+Apache Software License 2.0
+
+Copyright (c) 2020, Paul Ganssle (Google)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+
+## urllib3 (v2.4.0) - [MIT License](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
+
+```
+MIT License
+
+Copyright (c) 2008-2020 Andrey Petrov and contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## usd-core (v25.2.post1) - [Other/Proprietary License](https://openusd.org)
+
+```
+============================================================
+OpenUSD
+============================================================
+
+Note: The Tomorrow Open Source Technology License 1.0 differs from the
+original Apache License 2.0 in the following manner. Section 6 ("Trademarks")
+is different.
+
+TOMORROW OPEN SOURCE TECHNOLOGY LICENSE 1.0
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor
+ and its affiliates, except as required to comply with Section 4(c) of
+ the License and to reproduce the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+============================================================
+RapidJSON
+============================================================
+
+Tencent is pleased to support the open source community by making RapidJSON available.
+
+Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved.
+
+If you have downloaded a copy of the RapidJSON binary from Tencent, please note that the RapidJSON binary is licensed under the MIT License.
+If you have downloaded a copy of the RapidJSON source code from Tencent, please note that RapidJSON source code is licensed under the MIT License, except for the third-party components listed below which are subject to different license terms. Your integration of RapidJSON into your own projects may require compliance with the MIT License, as well as the other licenses applicable to the third-party components included within RapidJSON. To avoid the problematic JSON license in your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as it's the only code under the JSON license.
+A copy of the MIT License is included in this file.
+
+Other dependencies and licenses:
+
+Open Source Software Licensed Under the BSD License:
+--------------------------------------------------------------------
+
+The msinttypes r29
+Copyright (c) 2006-2013 Alexander Chemeris
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+* Neither the name of copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Open Source Software Licensed Under the JSON License:
+--------------------------------------------------------------------
+
+json.org
+Copyright (c) 2002 JSON.org
+All Rights Reserved.
+
+JSON_checker
+Copyright (c) 2002 JSON.org
+All Rights Reserved.
+
+
+Terms of the JSON License:
+---------------------------------------------------
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Terms of the MIT License:
+--------------------------------------------------------------------
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+============================================================
+double-conversion
+============================================================
+
+Copyright 2006-2011, the V8 project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+============================================================
+OpenEXR/IlmBase/Half
+============================================================
+
+///////////////////////////////////////////////////////////////////////////
+//
+// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas
+// Digital Ltd. LLC
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Industrial Light & Magic nor the names of
+// its contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+///////////////////////////////////////////////////////////////////////////
+
+============================================================
+libdeflate
+============================================================
+
+Copyright 2016 Eric Biggers
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation files
+(the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+============================================================
+Apple Technical Q&A QA1361 - Detecting the Debugger
+https://developer.apple.com/library/content/qa/qa1361/_index.html
+============================================================
+
+Sample code project: Detecting the Debugger
+Version: 1.0
+
+Abstract: Shows how to determine if code is being run under the debugger.
+
+IMPORTANT: This Apple software is supplied to you by Apple
+Inc. ("Apple") in consideration of your agreement to the following
+terms, and your use, installation, modification or redistribution of
+this Apple software constitutes acceptance of these terms. If you do
+not agree with these terms, please do not use, install, modify or
+redistribute this Apple software.
+
+In consideration of your agreement to abide by the following terms, and
+subject to these terms, Apple grants you a personal, non-exclusive
+license, under Apple's copyrights in this original Apple software (the
+"Apple Software"), to use, reproduce, modify and redistribute the Apple
+Software, with or without modifications, in source and/or binary forms;
+provided that if you redistribute the Apple Software in its entirety and
+without modifications, you must retain this notice and the following
+text and disclaimers in all such redistributions of the Apple Software.
+Neither the name, trademarks, service marks or logos of Apple Inc. may
+be used to endorse or promote products derived from the Apple Software
+without specific prior written permission from Apple. Except as
+expressly stated in this notice, no other rights or licenses, express or
+implied, are granted by Apple herein, including but not limited to any
+patent rights that may be infringed by your derivative works or by other
+works in which the Apple Software may be incorporated.
+
+The Apple Software is provided by Apple on an "AS IS" basis. APPLE
+MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
+THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
+OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
+
+IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
+MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
+AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
+STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+============================================================
+LZ4
+============================================================
+
+LZ4 - Fast LZ compression algorithm
+Copyright (C) 2011-present, Yann Collet.
+
+BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php)
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+You can contact the author at :
+ - LZ4 homepage : http://www.lz4.org
+ - LZ4 source repository : https://github.com/lz4/lz4
+
+============================================================
+stb
+============================================================
+
+stb_image - v2.19 - public domain image loader - http://nothings.org/stb
+ no warranty implied; use at your own risk
+
+stb_image_resize - v0.95 - public domain image resizing
+ by Jorge L Rodriguez (@VinoBS) - 2014
+ http://github.com/nothings/stb
+
+stb_image_write - v1.09 - public domain - http://nothings.org/stb/stb_image_write.h
+ writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015
+ no warranty implied; use at your own risk
+
+ALTERNATIVE B - Public Domain (www.unlicense.org)
+This is free and unencumbered software released into the public domain.
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+============================================================
+pugixml
+============================================================
+
+MIT License
+
+Copyright (c) 2006-2019 Arseny Kapoulkine
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+============================================================
+Vulkan C++ examples and demos (dome light texture filtering)
+============================================================
+
+The MIT License (MIT)
+
+Copyright (c) 2016 Sascha Willems
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+============================================================
+pbrt (Hammersley Low-Discrepancy Sampling Sequence)
+============================================================
+
+Copyright (c) 1998-2015, Matt Pharr, Greg Humphreys, and Wenzel Jakob.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+============================================================
+Draco
+============================================================
+USD bundles Draco, which is available under the Apache 2.0 license. For details,
+see https://github.com/google/draco/blob/master/README.md.
+
+
+============================================================
+Roboto Fonts
+============================================================
+USD bundles Roboto fonts, which is available under the Apache 2.0 license.
+For details, see https://fonts.google.com/specimen/Roboto#license
+
+
+============================================================
+Roboto Mono Fonts
+============================================================
+USD bundles Roboto Mono fonts, which is available under the Apache 2.0 license.
+For details, see https://fonts.google.com/specimen/Roboto+Mono#license
+
+
+============================================================
+Vulkan Memory Allocator
+============================================================
+Copyright (c) 2017-2020 Advanced Micro Devices, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+============================================================
+Spirv Reflect
+============================================================
+Copyright 2017-2018 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+============================================================
+khrplatform.h
+============================================================
+Copyright (c) 2008-2018 The Khronos Group Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and/or associated documentation files (the
+"Materials"), to deal in the Materials without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Materials, and to
+permit persons to whom the Materials are furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Materials.
+
+THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
+
+
+============================================================
+surfgrad-bump-standalone-demo
+============================================================
+MIT License
+
+Copyright (c) 2020 mmikk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+Tessil robin-map
+================================================================
+MIT License
+
+Copyright (c) 2017 Thibaut Goetghebuer-Planchon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=====
+CLI11
+=====
+
+CLI11 2.3.1 Copyright (c) 2017-2022 University of Cincinnati, developed by Henry
+Schreiner under NSF AWARD 1414736. All rights reserved.
+
+Redistribution and use in source and binary forms of CLI11, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+================================================================
+The Art of C++ : PEGTL (Parsing Expression Grammar Template Library)
+================================================================
+
+The MIT License (MIT)
+
+Copyright (c) 2007-2022 Dr. Colin Hirsch and Daniel Frey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+Doxygen Awesome
+================================================================
+
+MIT License
+
+Copyright (c) 2021 - 2023 jothepro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+LibAvif v1.0.4
+================================================================
+
+Copyright 2019 Joe Drago. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+AVIF/src/obu.c, renamed in this repository as AVIF/src/avif_obu.c
+================================================================
+
+Copyright ยฉ 2018-2019, VideoLAN and dav1d authors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+AVIF/third_party/libyuv/*
+================================================================
+
+Copyright 2011 The LibYuv Project Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+libaom
+================================================================
+
+Copyright (c) 2016, Alliance for Open Media. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+boost
+================================================================
+
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+```
+
+
+## utils3d (v0.0.2) - [MIT License](https://github.com/EasternJournalist/utils3d)
+
+```
+MIT License
+
+Copyright (c) 2022 EasternJournalist
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+
+## vomp (v0.1.0) - [Apache License](https://gitlab-master.nvidia.com/anitah/TRELLIS)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## warp-lang (v1.7.0) - [Apache License 2.0](https://github.com/NVIDIA/warp/blob/main/CHANGELOG.md)
+
+```
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
+
+
+## Werkzeug (v3.0.6) - [BSD License](https://opensource.org/licenses/BSD-3-Clause)
+
+```
+Copyright 2007 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## widgetsnbextension (v4.0.13) - [BSD License](http://jupyter.org)
+
+```
+Copyright (c) 2015 Project Jupyter Contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## wrapt (v1.17.2) - [BSD License](https://github.com/GrahamDumpleton/wrapt)
+
+```
+Copyright (c) 2013-2023, Graham Dumpleton
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## xformers (v0.0.27.post2+cu118) - [BSD License](https://facebookresearch.github.io/xformers/)
+
+```
+From xFormers:
+
+Copyright (c) Facebook, Inc. and its affiliates
+
+
+===
+
+BSD 3-Clause License
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America
+ and IDIAP Research Institute nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+```
+
+
+## zipp (v3.19.2) - [MIT License](https://github.com/jaraco/zipp)
+
+```
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+```
+
+
+## zstandard (v0.23.0) - [BSD License](https://github.com/indygreg/python-zstandard)
+
+```
+Copyright (c) 2016, Gregory Szorc
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
diff --git a/deps/vomp/CONTRIBUTING.md b/deps/vomp/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..5b94933b363f4c5edd105df2aec5f785ce01b7e4
--- /dev/null
+++ b/deps/vomp/CONTRIBUTING.md
@@ -0,0 +1,51 @@
+# How to Contribute
+
+We'd love to receive your patches and contributions. Please keep your PRs as draft until such time that you would like us to review them.
+
+## Code Reviews
+
+All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
+
+## Signing Your Work
+
+* We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license.
+
+ * Any contribution which contains commits that are not Signed-Off will not be accepted.
+
+* To sign off on a commit you simply use the `--signoff` (or `-s`) option when committing your changes:
+ ```bash
+ $ git commit -s -m "Add cool feature."
+ ```
+ This will append the following to your commit message:
+ ```
+ Signed-off-by: Your Name
+ ```
+
+* Full text of the DCO:
+
+ ```
+ Developer Certificate of Origin
+ Version 1.1
+
+ Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+ 1 Letterman Drive
+ Suite D4700
+ San Francisco, CA, 94129
+
+ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+ ```
+
+ ```
+ Developer's Certificate of Origin 1.1
+
+ By making a contribution to this project, I certify that:
+
+ (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or
+
+ (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or
+
+ (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it.
+
+ (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved.
+ ```
\ No newline at end of file
diff --git a/deps/vomp/LICENSE b/deps/vomp/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..56ee3c8c4cc2b4b32e0975d17258f9ba515fdbcc
--- /dev/null
+++ b/deps/vomp/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/deps/vomp/README.md b/deps/vomp/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e038a87702b15c7af32a183ea6252054de66f16c
--- /dev/null
+++ b/deps/vomp/README.md
@@ -0,0 +1,665 @@
+
+
VoMP: Predicting Volumetric Mechanical Properties
+
+

+

+

+

+
+
+
+
+This repository provides the implementation of **VoMP**. TL;DR: Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, etc. which can be used to produce realistic worlds. We recommend reading the [README_train.md](./README_train.md) if you need to fine-tune or train the model from scratch or know more details about the codebase.
+
+---
+
+## Contents
+
+- [๐ง Dependencies and Installation](#-dependencies-and-installation)
+ * [Setup a Virtual Environment (Recommended)](#setup-a-virtual-environment--recommended-)
+ * [Install a Mesh Renderer (Required for Mesh Processing Only)](#install-a-mesh-renderer--required-for-mesh-processing-only-)
+ + [Isaac Sim](#isaac-sim)
+ + [Blender](#blender)
+ * [Setup a Conda Environment (Alternative)](#setup-a-conda-environment--alternative-)
+ * [Trained Models](#trained-models)
+- [๐ Quickstart: Web Demo](#-quickstart-web-demo)
+- [๐ฅ Loading the Model](#-loading-the-model)
+ * [Using Inference Config (Recommended)](#using-inference-config--recommended-)
+ * [Using Direct File Paths](#using-direct-file-paths)
+ * [Using Directories (use for fine-tuning)](#using-directories--use-for-fine-tuning-)
+- [๐ฏ High-Level API](#-high-level-api)
+ * [Gaussian Splats](#gaussian-splats)
+ * [Meshes](#meshes)
+ * [USD Assets (including meshes)](#usd-assets--including-meshes-)
+ + [General USD Formats](#general-usd-formats)
+ + [SimReady Format USD](#simready-format-usd)
+- [๐จ Visualizing Material Results](#-visualizing-material-results)
+- [๐ง Low-Level API](#-low-level-api)
+ * [Gaussian Splats](#gaussian-splats-1)
+ * [Meshes](#meshes-1)
+ * [USD Assets](#usd-assets)
+- [๐งฉ Custom 3D Representations](#-custom-3d-representations)
+- [๐งฌ Material Upsampler](#-material-upsampler)
+- [๐พ Using our Benchmark](#-using-our-benchmark)
+ * [Reproducing results from the paper](#reproducing-results-from-the-paper)
+- [๐ฆ Simulation](#-simulation)
+ * [Simplicits simulation](#simplicits-simulation)
+ * [FEM simulation using warp.fem](#fem-simulation-using-warpfem)
+ * [FEM simulation using libuipc](#fem-simulation-using-libuipc)
+ * [Newton simulation](#newton-simulation)
+- [๐ค Credits](#-credits)
+- [๐ Citation](#-citation)
+- [License and Contact](#license-and-contact)
+
+## ๐ง Dependencies and Installation
+
+All the instructions in this README are meant to be run from the root of the repository. Running simulations requires a separate set of dependencies than this setup which we mention later in the [๐ฆ Simulation](#-simulation) section.
+
+### Setup a Virtual Environment (Recommended)
+
+First set up the environment. We recommend using Python>=3.10, PyTorch>=2.1.0, and CUDA>=11.8. It is okay if some packages show warnings or fail to install due to version conflicts. The version conflicts are not a problem for the functionalities we use.
+
+```bash
+git clone --recursive https://github.com/nv-tlabs/VoMP
+cd VoMP
+
+# Install dependencies using the provided script (Linux only)
+chmod +x install_env.sh
+./install_env.sh
+```
+
+> [!NOTE]
+> Running install_env.sh without conda: The script includes optional conda-only steps (environment creation/activation, installing CUDA toolkit inside the env, and setting env vars). If you're using a Python `venv` and don't have conda, the script will fail when it tries to call `conda`. You can either install conda, or comment out the conda-specific lines (lines 93-115 and any `conda install` / `conda env config vars set` commands). The rest of the script relies on `pip` and standard bash commands and will work in a `venv`.
+
+### Install a Mesh Renderer (Required for Mesh Processing Only)
+
+We only need a mesh renderer so you can download any one of Isaac Sim or Blender. There is no need to install both.
+
+#### Isaac Sim
+
+For mesh material estimation, you need to install Isaac Sim or Blender manually. *This is not required for Gaussian splat processing.*
+
+Download Isaac Sim from [here](https://docs.isaacsim.omniverse.nvidia.com/5.0.0/installation/index.html) and follow the instructions to install it. On Linux, you would have a `isaac-sim.sh` file in the path you installed it. For Windows, you would have a `isaac-sim.bat` file in the path you installed it. Note the path to the `isaac-sim.sh` or `isaac-sim.bat` file.
+
+> [!NOTE]
+> You'll need to provide the Isaac Sim binary path when using mesh APIs.
+
+> [!WARNING]
+> We use Replicator in Isaac Sim to render meshes. Replicator supports USD assets. If you want to use a USD asset, since USD files can contain many things in many formats we expect you to have used [existing tools](https://openusd.org/release/toolset.html) to convert it into an explicit mesh. If you want to use a mesh asset, you can use Replicator by also having a USD file of your mesh that you can make by using [existing tools](https://openusd.org/release/toolset.html).
+
+#### Blender
+
+For mesh material estimation, you need to install Blender 3.0+ manually. *This is not required for Gaussian splat processing.*
+
+```bash
+# Install system dependencies
+sudo apt-get update
+sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6
+
+# Download and install Blender 3.0.1
+wget https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz
+tar -xvf blender-3.0.1-linux-x64.tar.xz
+
+# Note the path: ./blender-3.0.1-linux-x64/blender
+```
+
+> [!NOTE]
+> You'll need to provide the Blender binary path when using mesh APIs:
+> ```python
+> results = model.get_mesh_materials("mesh.obj", blender_path="/path/to/blender")
+> ```
+
+### Setup a Conda Environment (Alternative)
+
+We also provide a conda environment file to install the dependencies. This automatically creates a new environment:
+
+```bash
+# Create and install environment from file (creates 'vomp' environment)
+conda env create -f environment.yml
+
+# Activate the environment
+conda activate vomp
+```
+
+> [!WARNING]
+> We do not recommend using this installation method. The conda environment file is accurate but it reflects the environment at its final stage and does not have the step-by-step process we use to install the dependencies.
+
+### Trained Models
+
+We provide the trained models (1.73 GB) in
. Download the models and place them in the `weights/` directory. The checkpoints we will use are the `weights/matvae.safetensors` and `weights/geometry_transformer.pt` files.
+
+The above two files from the model repository contains the final checkpoint of the model. If you need to fine-tune the model, you can follow the same process but download the `ft` directory from the HuggingFace repo too and place them in the `weights/` directory.
+
+| **File** | **Model** |
+|------|----------------|
+| `matvae.safetensors` | MatVAE |
+| `geometry_transformer.pt` | Geometry Transformer |
+| `normalization_params.json` | Normalization Parameters |
+| `inference.json` | Inference Configuration |
+| `ft` | MatVAE and Geometry Transformer checkpoints (same as above but in a directory structure compatible for fine-tuning) |
+
+## ๐ Quickstart: Web Demo
+
+We provide a simple web demo to quickly test out VoMP in a GUI. The web-demo uses some additional dependecies over the base environment, see [`gradio/requirements.txt`](./gradio/requirements.txt). To start the web demo, run:
+
+```bash
+python gradio/app.py
+```
+
+Then, you can access the demo at the address shown in the terminal. The web demo allows you to run the model, visualize the outputs of the model and download an artifact which can directly be used for [๐ฆ Simulation](#-simulation).
+
+## ๐ฅ Loading the Model
+
+Before using any of the VoMP APIs, you need to load the model. We provide flexible loading options:
+
+### Using Inference Config (Recommended)
+
+The simplest way to load the model is using the inference configuration file:
+
+```python
+from vomp.inference import Vomp
+
+# Load model using inference config (recommended - uses final_ckpt.zip weights)
+model = Vomp.from_checkpoint(
+ config_path="weights/inference.json",
+ use_trt=False # Set to True to enable TensorRT acceleration (significantly faster but requires `torch-tensorrt`)
+)
+```
+
+> [!NOTE]
+> Using the `use_trt` flag will compile the DINO model with TensorRT. This makes the `from_checkpoint` function slower.
+
+### Using Direct File Paths
+
+For more control, you can specify exact checkpoint files, optionally overriding the inference config:
+
+```python
+# Load model using direct file paths
+model = Vomp.from_checkpoint(
+ config_path="weights/inference.json",
+ geometry_checkpoint_dir="weights/geometry_transformer.pt",
+ matvae_checkpoint_dir="weights/matvae.safetensors",
+ normalization_params_path="weights/normalization_params.json"
+)
+
+# Or override specific paths from inference config
+model = Vomp.from_checkpoint(
+ config_path="configs/materials/inference.json",
+ geometry_checkpoint_dir="custom/path/to/geometry_transformer.pt" # Override just this path
+)
+```
+
+### Using Directories (use for fine-tuning)
+
+Use this approach only if you are using the fine-tuning checkpoints i.e. the `ft/` directory in the model repository. This lets the model auto-find the latest checkpoints:
+
+```python
+from vomp.inference import Vomp
+
+# Load model using directories (auto-finds latest checkpoints)
+model = Vomp.from_checkpoint(
+ config_path="weights/inference.json",
+ geometry_checkpoint_dir="weights/ft/geometry_transformer",
+ matvae_checkpoint_dir="weights/ft/matvae",
+ normalization_params_path="weights/ft/matvae/normalization_params.json",
+ geometry_ckpt="latest" # Can also be a specific step number
+)
+```
+
+We provide a flexible Python API with both high-level and low-level interfaces for material property estimation.
+
+## ๐ฏ High-Level API
+
+### Gaussian Splats
+
+For Gaussian splats, use the high-level API for the easiest experience (see [Loading the Model](#-loading-the-model) section first):
+
+```python
+from vomp.inference.utils import save_materials
+
+# Get materials directly from PLY (auto-handles Gaussian loading)
+# By default, returns materials evaluated at each Gaussian splat center
+results = model.get_splat_materials("path/to/your/gaussian_splat.ply")
+
+# Or use Kaolin voxelizer for more accurate results
+# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", voxel_method="kaolin")
+
+# Control where materials are evaluated using query_points:
+# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points="splat_centers") # Default
+# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points="voxel_centers") # Voxel centers (direct output of the model)
+# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points=custom_points) # Custom (N,3) array
+
+# Adjust DINO batch size for performance (higher values use more GPU memory)
+# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", dino_batch_size=32)
+
+# Save results
+save_materials(results, "materials.npz")
+```
+
+### Meshes
+
+For mesh objects, use the equivalent high-level mesh API (see [Loading the Model](#-loading-the-model) section first):
+
+```python
+from vomp.inference.utils import save_materials
+
+# Get materials directly from mesh file (supports OBJ, PLY, STL, USD)
+# By default, returns materials evaluated at each mesh vertex (not recommended if you have vertices only on the surface)
+# Note: Requires Blender installation (see Dependencies section)
+results = model.get_mesh_materials(
+ "path/to/your/mesh.obj",
+ blender_path="/tmp/blender-3.0.1-linux-x64/blender" # Adjust path as needed
+)
+
+# Control where materials are evaluated using query_points:
+# results = model.get_mesh_materials("path/to/your/mesh.obj", query_points="mesh_vertices") # Default
+# results = model.get_mesh_materials("path/to/your/mesh.obj", query_points="voxel_centers") # Voxel centers (direct output of the model)
+# results = model.get_mesh_materials("path/to/your/mesh.obj", query_points=custom_points) # Custom (N,3) array
+
+# Use parallel rendering and adjust DINO batch size for better performance
+# results = model.get_mesh_materials("path/to/your/mesh.obj", num_render_jobs=4, dino_batch_size=32, blender_path="/path/to/blender")
+
+# Save results
+save_materials(results, "materials.npz")
+```
+
+### USD Assets (including meshes)
+
+USD files can come in many different formats with varying internal structures, materials, and organization. For USD assets, use the high-level USD API (see [Loading the Model](#-loading-the-model) section first):
+
+#### General USD Formats
+
+For USD files in any format, use [Isaac Sim Replicator](https://docs.isaacsim.omniverse.nvidia.com/5.1.0/replicator_tutorials/index.html) rendering with a separate mesh file for voxelization:
+
+```python
+from vomp.inference.utils import save_materials
+
+# For general USD files - requires Isaac Sim and separate mesh
+# Note: Requires Isaac Sim installation and a separate mesh file for voxelization
+# Isaac Sim renders the USD while the mesh is used for voxelization
+results = model.get_usd_materials(
+ usd_path="path/to/your/model.usd",
+ mesh_path="path/to/your/model.ply", # Mesh for voxelization (doesn't need to be normalized)
+ isaac_sim_path="~/isaac-sim/isaac-sim.sh", # Or set ISAAC_SIM_PATH environment variable
+ render_mode="path_tracing" # Options: "fast" or "path_tracing"
+)
+
+# Control where materials are evaluated using query_points:
+# results = model.get_usd_materials(..., query_points="voxel_centers") # Default (direct output)
+# results = model.get_usd_materials(..., query_points=custom_points) # Custom (N,3) array
+
+# Adjust DINO batch size for performance (higher values use more GPU memory):
+# results = model.get_usd_materials(..., dino_batch_size=32)
+
+# Save results
+save_materials(results, "materials.npz")
+```
+
+Isaac Sim Replicator provides flexible rendering modes:
+
+```python
+# Option 1: Fast Mode - Real-time ray tracing
+results = model.get_usd_materials(
+ usd_path="model.usd",
+ mesh_path="model.ply",
+ isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ render_mode="fast" # Real-time ray tracing
+)
+
+# Option 2: Path Tracing - Higher quality
+results = model.get_usd_materials(
+ usd_path="model.usd",
+ mesh_path="model.ply",
+ isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ render_mode="path_tracing" # 256 spp, 8 bounces, denoising enabled
+)
+
+# Option 3: start from a setting and override some RTX settings
+from vomp.inference import RTX_PRESETS
+print(RTX_PRESETS.keys()) # See available presets: ['fast', 'path_tracing']
+
+results = model.get_usd_materials(
+ usd_path="model.usd",
+ mesh_path="model.ply",
+ isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ render_mode="path_tracing",
+ rtx_settings_override={
+ # Enable path tracing renderer
+ "/rtx/rendermode": "PathTracing",
+
+ # Path tracing quality settings
+ "/rtx/pathtracing/spp": 256, # Samples per pixel (higher = better quality, slower)
+ "/rtx/pathtracing/totalSpp": 256, # Total samples per pixel
+ "/rtx/pathtracing/maxBounces": 8, # Maximum light bounces
+ "/rtx/pathtracing/maxSpecularAndTransmissionBounces": 8,
+
+ # Additional quality settings
+ "/rtx/pathtracing/fireflyFilter/enable": True, # Reduce fireflies (bright pixels)
+ "/rtx/pathtracing/optixDenoiser/enabled": True, # Enable denoiser for clean renders
+
+ # ... other RTX settings you want to override
+ }
+)
+```
+
+> [!WARNING]
+> Please do not override the following RTX settings, as they are required for the model to work correctly:
+> - "/rtx/post/backgroundZeroAlpha/enabled": True,
+> - "/rtx/post/backgroundZeroAlpha/backgroundComposite": False,
+> - "/rtx/post/backgroundZeroAlpha/outputAlphaInComposite": True,
+
+#### SimReady Format USD
+
+If your USD file is in the **SimReady format** (like the USD files in our dataset), you can use the following arguments:
+
+```python
+from vomp.inference.utils import save_materials
+
+results = model.get_usd_materials(
+ usd_path="model.usd",
+ use_simready_usd_format=True,
+ blender_path="/path/to/blender",
+ seed=42
+)
+
+# Control where materials are evaluated using query_points:
+# results = model.get_usd_materials(..., query_points="voxel_centers") # Default (direct output)
+# results = model.get_usd_materials(..., query_points=custom_points) # Custom (N,3) array
+
+# Save results
+save_materials(results, "materials.npz")
+```
+
+## ๐จ Visualizing Material Results
+
+After estimating material properties, you can visualize them using our interactive `polyscope`-based viewer.
+
+```python
+# After getting results from any API
+from vomp.inference.utils import save_materials
+
+# Save your results
+save_materials(results, "my_materials.npz")
+```
+
+```bash
+# Launch interactive property viewer
+python scripts/viewer.py my_materials.npz
+```
+
+The viewer also saves the colorbars for visualizations as PNG images that look like this:
+
+
+
+## ๐ง Low-Level API
+
+### Gaussian Splats
+
+For fine-grained control with Gaussian splats, use the low-level API (see [Loading the Model](#-loading-the-model) section first):
+
+```python
+from vomp.representations.gaussian import Gaussian
+from vomp.inference.utils import save_materials
+
+# Load Gaussian splat
+gaussian = Gaussian(sh_degree=3, aabb=[-1,-1,-1,2,2,2], device="cuda")
+gaussian.load_ply("path/to/your/gaussian_splat.ply")
+
+# Step-by-step pipeline
+output_dir = "outputs/materials"
+renders_metadata = model.render_sampled_views(gaussian, output_dir, num_views=150)
+voxel_centers = model._voxelize_gaussian(gaussian, output_dir)
+coords, features = model._extract_dino_features(output_dir, voxel_centers, renders_metadata, save_features=True)
+results = model.predict_materials(coords, features)
+save_materials(results, "materials.npz")
+```
+
+### Meshes
+
+For fine-grained control with meshes, use the equivalent low-level mesh API (see [Loading the Model](#-loading-the-model) section first):
+
+```python
+from vomp.inference.utils import save_materials
+
+# Step-by-step pipeline for meshes
+output_dir = "outputs/materials"
+mesh_path = "path/to/your/mesh.obj"
+blender_path = "/tmp/blender-3.0.1-linux-x64/blender" # Adjust for your installation
+renders_metadata = model.render_mesh_views(mesh_path, output_dir, num_views=150, blender_path=blender_path)
+voxel_centers = model._voxelize_mesh(mesh_path, output_dir)
+coords, features = model._extract_dino_features(output_dir, voxel_centers, renders_metadata, save_features=True)
+results = model.predict_materials(coords, features)
+save_materials(results, "materials.npz")
+```
+
+### USD Assets
+
+For fine-grained control with USD assets using Replicator rendering (see [Loading the Model](#-loading-the-model) section first):
+
+```python
+from vomp.inference.utils import save_materials
+
+# Step-by-step pipeline for USD assets with Replicator
+output_dir = "outputs/materials"
+usd_path = "path/to/your/model.usd"
+mesh_path = "path/to/your/model.ply" # For voxelization
+isaac_sim_path = "~/isaac-sim/isaac-sim.sh"
+
+# Render using Replicator (with custom settings)
+renders_metadata = model.render_views_replicator(
+ asset_path=usd_path,
+ output_dir=output_dir,
+ num_views=150,
+ isaac_sim_path=isaac_sim_path,
+ render_mode="path_tracing", # or "fast"
+ rtx_settings_override={
+ "/rtx/pathtracing/spp": 512 # Optional: custom settings
+ }
+)
+
+# Voxelize and extract features
+voxel_centers = model._voxelize_mesh(mesh_path, output_dir)
+coords, features = model._extract_dino_features(output_dir, voxel_centers, renders_metadata, save_features=True)
+results = model.predict_materials(coords, features)
+save_materials(results, "materials.npz")
+```
+
+## ๐งฉ Custom 3D Representations
+
+Bring your own 3D representation with custom render/voxelize functions (see [Loading the Model](#-loading-the-model) section first):
+
+```python
+from vomp.inference.utils import save_materials
+
+def my_render_func(obj, output_dir, num_views, image_size, **kwargs):
+ # Your rendering code here - save images to output_dir/renders/
+ frames_metadata = []
+ for i in range(num_views):
+ # Your custom rendering logic
+ frames_metadata.append({
+ "file_path": f"frame_{i:04d}.png",
+ "transform_matrix": camera_matrix.tolist(), # 4x4 matrix
+ "camera_angle_x": fov_radians
+ })
+ return frames_metadata
+
+def my_voxelize_func(obj, output_dir, **kwargs):
+ # Your voxelization code here
+ voxel_centers = your_voxelization_method(obj) # (N, 3) array
+ return voxel_centers
+
+# Use with any 3D representation
+coords, features = model.get_features(
+ obj_3d=your_mesh,
+ render_func=my_render_func,
+ voxelize_func=my_voxelize_func,
+ num_views=150
+)
+
+# Get materials
+results = model.predict_materials(coords, features)
+save_materials(results, "materials.npz")
+```
+
+## ๐งฌ Material Upsampler
+
+The high-level splat API (`get_splat_materials`) automatically returns materials interpolated to splat centers. However, you may want to upsample materials to other locations like higher resolution grids or custom query points. We provide a utility class for these cases (see [Loading the Model](#-loading-the-model) section first).
+
+```python
+import numpy as np
+from vomp.inference.utils import MaterialUpsampler
+from vomp.representations.gaussian import Gaussian
+
+# Get voxel-level materials (needed for upsampling to custom locations)
+# Note: Use query_points="voxel_centers" to get voxel-level results
+voxel_results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points="voxel_centers")
+# OR for meshes
+# voxel_results = model.get_mesh_materials("path/to/your/mesh.obj", query_points="voxel_centers", blender_path="/path/to/blender")
+
+# Create upsampler from voxel-level results
+upsampler = MaterialUpsampler(
+ voxel_coords=voxel_results["voxel_coords_world"],
+ voxel_materials=np.column_stack([
+ voxel_results["youngs_modulus"],
+ voxel_results["poisson_ratio"],
+ voxel_results["density"]
+ ])
+)
+
+# Example 1: Interpolate to Gaussian centers manually (usually not needed - high-level API does this)
+gaussian = Gaussian(sh_degree=3, aabb=[-1,-1,-1,2,2,2], device="cuda")
+gaussian.load_ply("path/to/your/gaussian_splat.ply")
+gaussian_materials, gaussian_distances = upsampler.interpolate_to_gaussians(gaussian)
+
+# Example 2: Interpolate to higher resolution grid (128x128x128) - main use case for manual upsampling
+x = np.linspace(-0.5, 0.5, 128)
+xx, yy, zz = np.meshgrid(x, x, x)
+high_res_points = np.column_stack([xx.ravel(), yy.ravel(), zz.ravel()])
+high_res_materials, high_res_distances = upsampler.interpolate(high_res_points)
+
+# Example 3: Interpolate to custom query points - another main use case for manual upsampling
+query_points = np.random.uniform(-0.4, 0.4, size=(1000, 3))
+query_materials, query_distances = upsampler.interpolate(query_points)
+
+# Save results
+upsampler.save_results(gaussian.get_xyz.detach().cpu().numpy(), gaussian_materials,
+ gaussian_distances, "gaussian_materials.npz")
+upsampler.save_results(high_res_points, high_res_materials, high_res_distances, "high_res_materials.npz")
+upsampler.save_results(query_points, query_materials, query_distances, "custom_materials.npz")
+```
+
+## ๐พ Using our Benchmark
+
+> [!NOTE]
+> Due to licenses we are unable to make the vegetation subset of the dataset public. Thus, when you compare outputs to the paper make sure to compare them to the listed results on the "public dataset" (Table 2 and Table 3).
+
+We provide a dataset and a benchmark with fine-grained volumetric mechanical properties (65.9 GB) at
(or preprocess it yourself using the instructions in [README_train.md](./README_train.md)). We also provide code allowing the evaluation of models on this dataset.
+
+### Reproducing results from the paper
+
+Since our dataset is quite large, we provide a way to download only the test set by running the following command:
+
+```bash
+huggingface-cli download nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP-Eval --repo-type dataset --local-dir datasets/simready
+```
+
+We can now run VoMP on the test set:
+
+```bash
+python scripts/evaluate_geometry_encoder.py \
+ --config weights/inference.json \ # replace with your own config file
+ --checkpoint_dir weights/ft/geometry_transformer \ # replace with your own checkpoint directory
+ --data_dir datasets/simready \ # replace with your own data directory
+ # --ckpt latest \
+ # --results
+```
+
+This script requires loading the model in the ["Using Directories" method](#using-directories).
+
+This prints out many detailed metrics. Particularly, you can also make sure you can reproduce the main results from the paper by comparing Table 2 and Appendix Table 3 from the paper with the outputs from Section 5 (SUMMARY TABLES) of the results printed out.
+
+To build on top of our benchmark, you can replace the `load_model` and `evaluate_model` functions in the `scripts/evaluate_geometry_encoder.py` script with your own model and evaluation code.
+
+## ๐ฆ Simulation
+
+Our properties are compatible with all simulators. We provide instructions to run a few kinds of simulations with the properties.
+
+### Simplicits simulation
+
+For the large-scale simulations that we perform with [Simplicits](https://research.nvidia.com/labs/toronto-ai/simplicits/), refer to the [Simplicits](https://kaolin.readthedocs.io/en/latest/notes/simplicits.html) documentation.
+
+### FEM simulation using warp.fem
+
+We provide a way to run FEM simulations using [`warp.fem`](https://nvidia.github.io/warp/modules/fem.html).
+
+```bash
+cd simulation/warp.fem
+PYTHONPATH=./ python drop_tetmesh.py --mesh assets/cube_res20.msh --materials assets/cube_materials_two_halves.npz
+```
+
+This simple example has an artificially constructed NPZ file which can be used in `warp.fem`. This requires installing [`warp`](https://nvidia.github.io/warp/) and `meshio`.
+
+### FEM simulation using libuipc
+
+We provide a way to run FEM simulations using [`libuipc`](https://github.com/spiriMirror/libuipc/). These simulations use the config files in the `configs/sim/` directory and they can be run as,
+
+```bash
+python vomp/sim/main.py configs/sim/falling_oranges.json
+```
+
+This config runs a simulation of falling oranges (Figure 5 from the paper) with the NPZ files we generated from the VoMP model.
+
+These simulations require a `.npz` file with the estimated mechanical properties of the object. This requires installing the Python version of `libuipc` using the instructions in the [`libuipc`](https://github.com/spiriMirror/libuipc/) repository. The command above will run the simulation, show it in a GUI, and save framewise surface meshes in the `outputs/simulation_output/falling_oranges` directory. The config also specifies a visual textured surface mesh so the per frame visualizations will use the high resolution visual mesh and also have textures.
+
+### Newton simulation
+
+We provide a way to run [Newton](https://github.com/newton-physics/newton/) simulations. Run an example simulation of a soft body cube with the NPZ files we generated from the VoMP model by running the following command:
+
+```bash
+cd simulation/newton
+python mesh_falling_sim.py --grid_dim 16 --materials cube_high_E.npz
+python mesh_falling_sim.py --grid_dim 16 --materials cube_low_E.npz
+```
+
+This simple example has two artificially constructed NPZ files which can be used in Newton. Observe the difference in simulation showing all Young's modulus, Poisson's ratio, and density values were properly applied. This requires installing [`newton`](https://github.com/newton-physics/newton/) and `meshio`.
+
+> [!NOTE]
+> Our properties are also compatible with [PhysX](https://developer.nvidia.com/physx-sdk) and rigid-body simulators. We plan to release some example code to do so at a later date. Until then, if you want to use our properties in PhysX, we recommend clustering the properties we produce, split the underlying meshes based on the clusters, and then add the averaged property for each such "connected part".
+
+## ๐ค Credits
+
+We are also grateful to several other open-source repositories that we drew inspiration from or built upon during the development of our pipeline:
+
+- [DINOv2](https://github.com/facebookresearch/dinov2)
+- [fTetWild](https://github.com/wildmeshing/fTetWild)
+- [gaussian-splatting](https://github.com/graphdeco-inria/gaussian-splatting)
+- [Isaac Sim](https://developer.nvidia.com/isaac/sim)
+- [kaolin](https://github.com/NVIDIAGameWorks/kaolin)
+- [libuipc](https://github.com/spiriMirror/libuipc)
+- [newton](https://github.com/newton-physics/newton)
+- [Simplicits](https://research.nvidia.com/labs/toronto-ai/simplicits/)
+- [textgrad](https://github.com/zou-group/textgrad)
+- [TRELLIS](https://github.com/microsoft/TRELLIS)
+- [Warp](https://nvidia.github.io/warp/)
+
+## ๐ Citation
+
+If you find VoMP helpful, please consider citing:
+
+```bibtex
+@inproceedings{dagli2026vomp,
+ title={Vo{MP}: Predicting Volumetric Mechanical Property Fields},
+ author={Rishit Dagli and Donglai Xiang and Vismay Modi and Charles Loop and Clement Fuji Tsang and Anka He Chen and Anita Hu and Gavriel State and David Levin I.W. and Maria Shugrina},
+ booktitle={The Fourteenth International Conference on Learning Representations},
+ year={2026},
+url={https://openreview.net/forum?id=aTP1IM6alo}
+}
+```
+
+## License and Contact
+
+This project will download and install additional third-party open source software projects. Review the license terms of these open source projects before use.
+
+VoMP source code is released under the [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0).
+
+VoMP models are released under the [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license). For a custom license, please visit our website and submit the form: [NVIDIA Research Licensing](https://www.nvidia.com/en-us/research/inquiries/).
\ No newline at end of file
diff --git a/deps/vomp/README_train.md b/deps/vomp/README_train.md
new file mode 100644
index 0000000000000000000000000000000000000000..15e4c81382d6a719962965920ca11f41da858a35
--- /dev/null
+++ b/deps/vomp/README_train.md
@@ -0,0 +1,285 @@
+
+
VoMP: Predicting Volumetric Mechanical Properties
+
+

+

+

+

+
+
+
+
+This repository provides the implementation of **VoMP**. TL;DR: Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, etc. which can be used to produce realistic worlds.
+
+---
+
+## Contents
+
+- [๐ง Setup](#-setup)
+- [๐ Overview of the codebase](#-overview-of-the-codebase)
+- [๐ Create the dataset](#-create-the-dataset)
+ * [Preprocessed Datasets](#preprocessed-datasets)
+ * [Material Triplet Dataset (MTD)](#material-triplet-dataset--mtd-)
+ * [Geometry with Volumetric Materials (GVM)](#geometry-with-volumetric-materials--gvm-)
+ * [Preparing your own data for training the Geometry Transformer](#preparing-your-own-data-for-training-the-geometry-transformer)
+- [๐ป Training](#-training)
+ * [Training the MatVAE](#training-the-matvae)
+ * [Training the Geometry Transformer](#training-the-geometry-transformer)
+ * [Training on your own data](#training-on-your-own-data)
+ * [Fine-tuning](#fine-tuning)
+- [๐ก Tips](#-tips)
+
+## ๐ง Setup
+
+Follow the instructions in the [README.md](./README.md) file to set up the environment.
+
+## ๐ Overview of the codebase
+
+
+
+The codebase is organized as follows:
+
+- `train_material_vae.py`: Main entry point for training the MatVAE.
+- `train_geometry_encoder.py`: Main entry point for training the Geometry Transformer.
+- `vomp/`: Main Python package containing all models and utilities.
+ - `models/`: Neural network architectures including MatVAE and Geometry Transformer.
+ - `geometry_encoder.py`: Geometry Transformer encoder.
+ - `material_vae/`: MatVAE model implementations.
+ - `structured_latent_vae/`: Structured latent VAE components.
+ - `trainers/`: Training frameworks for different model types.
+ - `modules/`: Neural network layer classes (sparse transformers, attention, etc.).
+ - `datasets/`: Dataset loaders (`SparseVoxelMaterials`, etc.).
+ - `representations/`: 3D representation handlers (Gaussian splats).
+ - `inference/`: Inference pipeline (`vomp.py`) and utilities.
+ - `utils/`: General utility functions and data processing tools.
+- `dataset_toolkits/`: Tools for dataset creation and preprocessing.
+ - `material_objects/`: Material property rendering, voxelization, and VLM annotation tools.
+ - `datasets/`: Dataset loaders (simready, ABO, etc.).
+- `configs/`: Configuration files for different experiments.
+ - `materials/`: MatVAE and Geometry Transformer configurations.
+- `scripts/`: Visualization and evaluation scripts.
+- `weights/`: Directory for storing pretrained model weights.
+
+## ๐ Create the dataset
+
+We provide toolkits for data preparation.
+
+
+
+### Preprocessed Datasets
+
+We provide the preprocessed datasets (with the vegetation subset removed) at:
. We are unable to make the MTD dataset public due to licenses.
+
+### Material Triplet Dataset (MTD)
+
+First compile the `material_ranges.csv` file by extracting data from the following sources (and deduplicate the data):
+
+- [MatWeb](https://matweb.com/)
+- [Engineering Toolbox](https://www.engineeringtoolbox.com/engineering-materials-properties-d_1225.html)
+- [Cambridge University Press](https://teaching.eng.cam.ac.uk/sites/teaching.eng.cam.ac.uk/files/Documents/Databooks/MATERIALS%20DATABOOK%20(2011)%20version%20for%20Moodle.pdf)
+
+The Material Triplet Dataset (MTD) is used to train the MatVAE. Assuming you have the `material_ranges.csv` file in the `datasets/latent_space/` directory, you can create the MTD by running the following command:
+
+```bash
+python dataset_toolkits/latent_space/make_csv.py datasets/latent_space/
+```
+
+Due to the dataset licenses, we cannot provide the `material_ranges.csv` file.
+
+### Geometry with Volumetric Materials (GVM)
+
+The Geometry with Volumetric Materials (GVM) is used to train the Geometry Transformer. First, download the following datasets to `datasets/raw/`:
+
+- [SimReady (13.9 GB + 20.5 GB + 9.4 GB + 21.4 GB + 20.6 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#simready-warehouse-01-assets-pack)
+- [Commercial (5.8 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#commercial-assets-pack)
+- [Residential (22.5 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#residential-assets-pack)
+- [Vegetation (2.7 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#vegetation-assets-pack)
+
+> [!NOTE]
+> The SimReady dataset is split into 5 parts. You can download them all from the aforementioned URL.
+
+Next, unzip these datasets to `datasets/raw/`, to create a directory structure like:
+
+```
+datasets/raw/
+โโโ simready/
+โโโ commercial/
+โโโ residential/
+โโโ vegetation/
+```
+
+Then, run the following command to create the GVM. This step takes ~2.5 days on 2 A100 GPUs, assuming you have enough CPU resources, as we perform significant CPU rendering.
+
+```bash
+mkdir -p /tmp/vlm
+
+python dataset_toolkits/material_objects/vlm_annotations/main.py \
+ --dataset simready residential commercial vegetation \
+ -o datasets/raw/material_annotations.json \
+ --verbose
+```
+
+The VLM prompt is optimized using the `scripts/optimize_prompt.py` script which requires installing [textgrad](https://github.com/zou-group/textgrad).
+
+This saves the annotations to `datasets/raw/material_annotations.json` in the following format.
+
+```json
+[
+ {
+ "object_name": "aluminumpallet_a01",
+ "category": "pallet",
+ "dataset_type": "simready",
+ "segments": {
+ "SM_AluminumPallet_A01_01": {
+ "name": "default__metal__aluminumpallet_a01",
+ "opacity": "opaque",
+ "material_type": "metal",
+ "semantic_usage": "aluminumpallet_a01",
+ "density": 2700.0,
+ "dynamic_friction": 0.1,
+ "static_friction": 0.1,
+ "restitution": 0.1,
+ "textures": {
+ "albedo": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/textures/T_Aluminium_Brushed_A1_Albedo.png",
+ "orm": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/textures/T_Aluminium_Brushed_A1_ORM.png",
+ "normal": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/textures/T_Aluminium_Brushed_A1_Normal.png"
+ },
+ "vlm_analysis": "...",
+ "youngs_modulus": 70000000000.0,
+ "poissons_ratio": 0.33
+ }
+ },
+ "file_path": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/aluminumpallet_a01_inst_base.usd"
+ },
+ ...
+]
+```
+
+### Preparing your own data for training the Geometry Transformer
+
+To train VoMP on your own data, you need to prepare a dataset of 3D objects with volumetric materials. Particularly, you need to prepare a JSON file and USD files with the following format:
+
+```json
+[
+ {
+ "object_name": "[object name]",
+ "segments": {
+ "[segment name that matches the segment name in the USD file]": {
+ "density": 2700.0,
+ "youngs_modulus": 70000000000.0,
+ "poissons_ratio": 0.33
+ }
+ },
+ "file_path": "path/to/your/object.usd"
+ }
+ ...
+]
+```
+
+If you are preparing your own dataset make sure the individual segments you list in the JSON file match the segment names in the USD file and each segment is a mesh. Also make sure the object has appearance properties. The workflow would work even if you do not have appearance properties, but the estimated properties would be significantly worse.
+
+## ๐ป Training
+
+### Training the MatVAE
+
+First run `accelerate` config to create a config file, setting your hardware details and if you want to do distributed training. We highly recommend using a single GPU for training MatVAE. This step takes ~12 hours on a single A100 GPU.
+
+Training hyperparameters and model architectures are defined in configuration files under the `configs/` directory. Example configuration files include:
+
+| **Config** | **Description** |
+|------------|-----------------|
+| `configs/materials/material_vae/matvae.json` | Training configuration for MatVAE. |
+| ... | Training configuration for ablations. |
+
+Any configuration file can be used to start training (use `accelerate launch` instead of `python` if you want to do distributed training),
+
+```bash
+python train_material_vae.py --config ...
+```
+
+Train the MatVAE by running the following command:
+
+```bash
+python train_material_vae.py --config configs/materials/material_vae/matvae.json
+```
+
+This creates the `outputs/matvae/` directory, which contains the trained model and tensorboard logs.
+
+### Training the Geometry Transformer
+
+First, start by performing data preprocessing. This step takes ~2 days on an A100 GPU + ~1.5 days on an RTX6000 GPU (used for rendering).
+
+```bash
+# python dataset_toolkits/build_metadata.py simready --output_dir datasets/simready
+python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready
+
+# Render USD files to images (can be parallelized across GPUs)
+# For multi-GPU: use --rank and --world_size arguments
+# Example: python ... --rank 0 --world_size 4 (run on GPU 0)
+# python ... --rank 1 --world_size 4 (run on GPU 1), etc.
+python dataset_toolkits/material_objects/render_usd.py allmats --output_dir datasets/simready --quiet --max_workers 3
+
+python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready --from_file
+python dataset_toolkits/material_objects/voxelize.py --output_dir datasets/simready --max_voxels 72000 --force
+python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready --from_file
+
+python dataset_toolkits/extract_feature.py --output_dir datasets/simready --force
+python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready
+```
+
+This creates the `datasets/simready/` directory, which contains the preprocessed data.
+
+```bash
+datasets/simready
+โโโ features (outputs from DINOv2 feature aggregation)
+โโโ merged_records
+โโโ metadata.csv
+โโโ renders (150 rendered images per object with camera poses)
+โโโ splits (train/val/test splits)
+โโโ statistics.txt (statistics of the dataset)
+โโโ voxels (voxelized meshes and voxel-wise mechanical properties)
+```
+
+Next, run the following command to train the Geometry Transformer. This step takes ~5 days on 4 A100 GPUs.
+
+```bash
+python train_geometry_encoder.py --config configs/materials/geometry_encoder/train.json --output_dir outputs/geometry_encoder
+```
+
+This creates the `outputs/geometry_encoder/` directory, which contains the trained model and tensorboard logs.
+
+### Training on your own data
+
+Once you have prepared your dataset following the format above, training is straightforward.
+
+```bash
+python train_geometry_encoder.py --config ... --output_dir ...
+```
+
+Replace the config and output directory with your own. You can make a new config file by copying one of the existing ones in the `configs/` directory and modifying the hyperparameters and dataset paths.
+
+### Fine-tuning
+
+Fine-tuning from pre-trained checkpoints is built into the training pipeline, simply run the following command:
+
+```bash
+python train_geometry_encoder.py --config ... --output_dir ...
+```
+
+It searches for models in the `outputs/geometry_encoder/ckpts/` directory in the following format `geometry_encoder_step[0-9]+.pt` and uses it to continue training.
+
+```bash
+โโโ geometry_encoder_ema0.9999_step0060000.pt
+โโโ geometry_encoder_ema0.9999_step0200000.pt
+โโโ geometry_encoder_step0060000.pt
+โโโ geometry_encoder_step0200000.pt
+โโโ misc_step0060000.pt
+โโโ misc_step0200000.pt
+```
+
+It also optionally searches for the `misc_step[0-9]+.pt` file to restore the optimizer state and scheduler state as well as `geometry_encoder_ema0.9999_step[0-9]+.pt` to restore the EMA model weights.
+
+## ๐ก Tips
+
+- Running the model requires 40 GB VRAM. If you often run into out of memory errors, you can reduce the amount of voxels we use for the object.
+- Dataset annotation with a VLM uses Qwen2.5-VL-72B which requires ~138 GB VRAM even when you load it in BF16 precision. The dataset annotation was done on 2 A100 GPUs. If you often run into out of memory errors, you can swap for a smaller version of Qwen2.5-VL or some other model, though the annotation would likely be degraded.
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/geometry_encoder/train.json b/deps/vomp/configs/materials/geometry_encoder/train.json
new file mode 100644
index 0000000000000000000000000000000000000000..9b0b1433b39e3c7f68628aecb8e2f023387b637c
--- /dev/null
+++ b/deps/vomp/configs/materials/geometry_encoder/train.json
@@ -0,0 +1,83 @@
+{
+ "matvae_checkpoint": "outputs/matvae2/checkpoints/checkpoint_853/model.safetensors",
+ "trellis_weights_path": "weights/TRELLIS-image-large",
+ "models": {
+ "geometry_encoder": {
+ "name": "geometry_encoder",
+ "args": {
+ "resolution": 64,
+ "in_channels": 1024,
+ "model_channels": 768,
+ "latent_channels": 2,
+ "num_blocks": 12,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "matvae": {
+ "name": "matvae",
+ "args": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ }
+ }
+ },
+ "dataset": {
+ "name": "SparseVoxelMaterials",
+ "normalization_type": "log_minmax",
+ "args": {
+ "roots": "datasets/simready",
+ "image_size": 512,
+ "model": "dinov2_vitl14_reg",
+ "resolution": 64,
+ "min_aesthetic_score": 0.0,
+ "max_num_voxels": 32768,
+ "compute_material_stats": false
+ }
+ },
+ "trainer": {
+ "name": "SLatVaeMaterialsTrainer",
+ "args": {
+ "max_steps": 1000000,
+ "batch_size_per_gpu": 16,
+ "batch_split": 1,
+ "optimizer": {
+ "name": "AdamW",
+ "args": {
+ "lr": 1e-4,
+ "weight_decay": 0.0
+ }
+ },
+ "ema_rate": [
+ 0.9999
+ ],
+ "fp16_mode": "inflat_all",
+ "fp16_scale_growth": 0.001,
+ "elastic": {
+ "name": "LinearMemoryController",
+ "args": {
+ "target_ratio": 0.75,
+ "max_mem_ratio_start": 0.5
+ }
+ },
+ "grad_clip": {
+ "name": "AdaptiveGradClipper",
+ "args": {
+ "max_norm": 1.0,
+ "clip_percentile": 95
+ }
+ },
+ "i_log": 10,
+ "i_save": 2000,
+ "i_eval": 1000,
+ "loss_type": "l1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_direct.json b/deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_direct.json
new file mode 100644
index 0000000000000000000000000000000000000000..3f8d2d4684f7f89d941aebf64d1ff5747b5a5932
--- /dev/null
+++ b/deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_direct.json
@@ -0,0 +1,99 @@
+{
+ "training_mode": "encoder_decoder_direct",
+ "matvae_checkpoint": "outputs/matvae/checkpoints/checkpoint_821/model.safetensors",
+ "trellis_weights_path": "weights/TRELLIS-image-large",
+ "models": {
+ "geometry_encoder": {
+ "name": "geometry_encoder",
+ "args": {
+ "resolution": 64,
+ "in_channels": 1024,
+ "model_channels": 768,
+ "latent_channels": 8,
+ "num_blocks": 12,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "decoder": {
+ "name": "decoder",
+ "args": {
+ "resolution": 64,
+ "model_channels": 768,
+ "latent_channels": 8,
+ "num_blocks": 12,
+ "out_channels": 3,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "matvae": {
+ "name": "matvae",
+ "args": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ }
+ }
+ },
+ "dataset": {
+ "name": "SparseVoxelMaterials",
+ "normalization_type": "log_minmax",
+ "args": {
+ "roots": "datasets/simready",
+ "image_size": 512,
+ "model": "dinov2_vitl14_reg",
+ "resolution": 64,
+ "min_aesthetic_score": 0.0,
+ "max_num_voxels": 32768,
+ "compute_material_stats": false
+ }
+ },
+ "trainer": {
+ "name": "SLatVaeMaterialsTrainer",
+ "args": {
+ "max_steps": 1000000,
+ "batch_size_per_gpu": 16,
+ "batch_split": 1,
+ "optimizer": {
+ "name": "AdamW",
+ "args": {
+ "lr": 1e-4,
+ "weight_decay": 0.0
+ }
+ },
+ "ema_rate": [
+ 0.9999
+ ],
+ "fp16_mode": "inflat_all",
+ "fp16_scale_growth": 0.001,
+ "elastic": {
+ "name": "LinearMemoryController",
+ "args": {
+ "target_ratio": 0.75,
+ "max_mem_ratio_start": 0.5
+ }
+ },
+ "grad_clip": {
+ "name": "AdaptiveGradClipper",
+ "args": {
+ "max_norm": 1.0,
+ "clip_percentile": 95
+ }
+ },
+ "i_log": 10,
+ "i_save": 1000,
+ "i_eval": 1000,
+ "loss_type": "l1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_matvae.json b/deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_matvae.json
new file mode 100644
index 0000000000000000000000000000000000000000..d05339b48b8077055b3e0629d88011c9a9e916a3
--- /dev/null
+++ b/deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_matvae.json
@@ -0,0 +1,99 @@
+{
+ "training_mode": "encoder_decoder_matvae",
+ "matvae_checkpoint": "outputs/matvae/checkpoints/checkpoint_821/model.safetensors",
+ "trellis_weights_path": "weights/TRELLIS-image-large",
+ "models": {
+ "geometry_encoder": {
+ "name": "geometry_encoder",
+ "args": {
+ "resolution": 64,
+ "in_channels": 1024,
+ "model_channels": 768,
+ "latent_channels": 8,
+ "num_blocks": 12,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "decoder": {
+ "name": "decoder",
+ "args": {
+ "resolution": 64,
+ "model_channels": 768,
+ "latent_channels": 8,
+ "num_blocks": 12,
+ "out_channels": 2,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "matvae": {
+ "name": "matvae",
+ "args": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ }
+ }
+ },
+ "dataset": {
+ "name": "SparseVoxelMaterials",
+ "normalization_type": "log_minmax",
+ "args": {
+ "roots": "datasets/simready",
+ "image_size": 512,
+ "model": "dinov2_vitl14_reg",
+ "resolution": 64,
+ "min_aesthetic_score": 0.0,
+ "max_num_voxels": 32768,
+ "compute_material_stats": false
+ }
+ },
+ "trainer": {
+ "name": "SLatVaeMaterialsTrainer",
+ "args": {
+ "max_steps": 1000000,
+ "batch_size_per_gpu": 16,
+ "batch_split": 1,
+ "optimizer": {
+ "name": "AdamW",
+ "args": {
+ "lr": 1e-4,
+ "weight_decay": 0.0
+ }
+ },
+ "ema_rate": [
+ 0.9999
+ ],
+ "fp16_mode": "inflat_all",
+ "fp16_scale_growth": 0.001,
+ "elastic": {
+ "name": "LinearMemoryController",
+ "args": {
+ "target_ratio": 0.75,
+ "max_mem_ratio_start": 0.5
+ }
+ },
+ "grad_clip": {
+ "name": "AdaptiveGradClipper",
+ "args": {
+ "max_norm": 1.0,
+ "clip_percentile": 95
+ }
+ },
+ "i_log": 10,
+ "i_save": 1000,
+ "i_eval": 1000,
+ "loss_type": "l1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/geometry_encoder/train_standard.json b/deps/vomp/configs/materials/geometry_encoder/train_standard.json
new file mode 100644
index 0000000000000000000000000000000000000000..dbcbc7fd5b519663c7eb699ce9e657e1eb18796e
--- /dev/null
+++ b/deps/vomp/configs/materials/geometry_encoder/train_standard.json
@@ -0,0 +1,83 @@
+{
+ "matvae_checkpoint": "outputs/matvae2/checkpoints/checkpoint_853/model.safetensors",
+ "trellis_weights_path": "weights/TRELLIS-image-large",
+ "models": {
+ "geometry_encoder": {
+ "name": "geometry_encoder",
+ "args": {
+ "resolution": 64,
+ "in_channels": 1024,
+ "model_channels": 768,
+ "latent_channels": 2,
+ "num_blocks": 12,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "matvae": {
+ "name": "matvae",
+ "args": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ }
+ }
+ },
+ "dataset": {
+ "name": "SparseVoxelMaterials",
+ "normalization_type": "standard",
+ "args": {
+ "roots": "datasets/simready",
+ "image_size": 512,
+ "model": "dinov2_vitl14_reg",
+ "resolution": 64,
+ "min_aesthetic_score": 0.0,
+ "max_num_voxels": 32768,
+ "compute_material_stats": false
+ }
+ },
+ "trainer": {
+ "name": "SLatVaeMaterialsTrainer",
+ "args": {
+ "max_steps": 1000000,
+ "batch_size_per_gpu": 16,
+ "batch_split": 1,
+ "optimizer": {
+ "name": "AdamW",
+ "args": {
+ "lr": 1e-4,
+ "weight_decay": 0.0
+ }
+ },
+ "ema_rate": [
+ 0.9999
+ ],
+ "fp16_mode": "inflat_all",
+ "fp16_scale_growth": 0.001,
+ "elastic": {
+ "name": "LinearMemoryController",
+ "args": {
+ "target_ratio": 0.75,
+ "max_mem_ratio_start": 0.5
+ }
+ },
+ "grad_clip": {
+ "name": "AdaptiveGradClipper",
+ "args": {
+ "max_norm": 1.0,
+ "clip_percentile": 95
+ }
+ },
+ "i_log": 10,
+ "i_save": 2000,
+ "i_eval": 1000,
+ "loss_type": "l1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/inference.json b/deps/vomp/configs/materials/inference.json
new file mode 100644
index 0000000000000000000000000000000000000000..577b7966500995a5457290dd560bd635d9429b54
--- /dev/null
+++ b/deps/vomp/configs/materials/inference.json
@@ -0,0 +1,86 @@
+{
+ "geometry_checkpoint_dir": "weights/geometry_transformer.pt",
+ "matvae_checkpoint_dir": "weights/matvae.safetensors",
+ "normalization_params_path": "weights/normalization_params.json",
+ "matvae_checkpoint": "weights/matvae.safetensors",
+ "trellis_weights_path": "weights/TRELLIS-image-large",
+ "models": {
+ "geometry_encoder": {
+ "name": "geometry_encoder",
+ "args": {
+ "resolution": 64,
+ "in_channels": 1024,
+ "model_channels": 768,
+ "latent_channels": 2,
+ "num_blocks": 12,
+ "num_heads": 12,
+ "mlp_ratio": 4,
+ "attn_mode": "swin",
+ "window_size": 8,
+ "use_fp16": true
+ }
+ },
+ "matvae": {
+ "name": "matvae",
+ "args": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ }
+ }
+ },
+ "dataset": {
+ "name": "SparseVoxelMaterials",
+ "normalization_type": "log_minmax",
+ "args": {
+ "roots": "datasets/simready",
+ "image_size": 512,
+ "model": "dinov2_vitl14_reg",
+ "resolution": 64,
+ "min_aesthetic_score": 0.0,
+ "max_num_voxels": 32768,
+ "compute_material_stats": false
+ }
+ },
+ "trainer": {
+ "name": "SLatVaeMaterialsTrainer",
+ "args": {
+ "max_steps": 1000000,
+ "batch_size_per_gpu": 16,
+ "batch_split": 1,
+ "optimizer": {
+ "name": "AdamW",
+ "args": {
+ "lr": 1e-4,
+ "weight_decay": 0.0
+ }
+ },
+ "ema_rate": [
+ 0.9999
+ ],
+ "fp16_mode": "inflat_all",
+ "fp16_scale_growth": 0.001,
+ "elastic": {
+ "name": "LinearMemoryController",
+ "args": {
+ "target_ratio": 0.75,
+ "max_mem_ratio_start": 0.5
+ }
+ },
+ "grad_clip": {
+ "name": "AdaptiveGradClipper",
+ "args": {
+ "max_norm": 1.0,
+ "clip_percentile": 95
+ }
+ },
+ "i_log": 10,
+ "i_save": 2000,
+ "i_eval": 1000,
+ "loss_type": "l1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/beta_tc_final.json b/deps/vomp/configs/materials/material_vae/beta_tc_final.json
new file mode 100644
index 0000000000000000000000000000000000000000..d9d0a6137f19bd61b299771a97667cc39a6713c7
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/beta_tc_final.json
@@ -0,0 +1,68 @@
+{
+ "dry_run": false,
+ "standard_vae": false,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/beta_tc",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 25000,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 5e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+ "iwae_K": 50,
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 512,
+ "depth": 4,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_flow": false
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/matvae.json b/deps/vomp/configs/materials/material_vae/matvae.json
new file mode 100644
index 0000000000000000000000000000000000000000..83f54c79a815b64e8553360d255bcfc14f63e5a0
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae.json
@@ -0,0 +1,76 @@
+{
+ "dry_run": false,
+ "standard_vae": false,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "log_minmax",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/matvae_log_minmax_no_density.json b/deps/vomp/configs/materials/material_vae/matvae_log_minmax_no_density.json
new file mode 100644
index 0000000000000000000000000000000000000000..15aae69fa68470a568f23ae2a9cf2781cd96deb3
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae_log_minmax_no_density.json
@@ -0,0 +1,76 @@
+{
+ "dry_run": false,
+ "standard_vae": false,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "log_minmax_no_density",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
diff --git a/deps/vomp/configs/materials/material_vae/matvae_no_beta_tc.json b/deps/vomp/configs/materials/material_vae/matvae_no_beta_tc.json
new file mode 100644
index 0000000000000000000000000000000000000000..f8503782610c7bed502fb0cee9a905bce3bb5094
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae_no_beta_tc.json
@@ -0,0 +1,77 @@
+{
+ "dry_run": false,
+ "standard_vae": true,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae_ablations/no_beta_tc",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "log_minmax",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true,
+ "use_flow": true
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/matvae_no_flow.json b/deps/vomp/configs/materials/material_vae/matvae_no_flow.json
new file mode 100644
index 0000000000000000000000000000000000000000..8dedf8dbb145ba5dafc839e999ab680e84bb272a
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae_no_flow.json
@@ -0,0 +1,77 @@
+{
+ "dry_run": false,
+ "standard_vae": false,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae_ablations/no_flow",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "log_minmax",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true,
+ "use_flow": false
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/matvae_no_free_nats.json b/deps/vomp/configs/materials/material_vae/matvae_no_free_nats.json
new file mode 100644
index 0000000000000000000000000000000000000000..0b1d7bcefaed996aee6b67340c9c8fd02096a943
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae_no_free_nats.json
@@ -0,0 +1,76 @@
+{
+ "dry_run": false,
+ "standard_vae": false,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae_ablations/no_free_nats",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.0,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "log_minmax",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/matvae_standard.json b/deps/vomp/configs/materials/material_vae/matvae_standard.json
new file mode 100644
index 0000000000000000000000000000000000000000..62ed067e95878c52dffb88ed2cdd11d5a971985e
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae_standard.json
@@ -0,0 +1,76 @@
+{
+ "dry_run": false,
+ "standard_vae": true,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "log_minmax",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+ }
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/matvae_standard_norm.json b/deps/vomp/configs/materials/material_vae/matvae_standard_norm.json
new file mode 100644
index 0000000000000000000000000000000000000000..e29928a8bc42af6b25bfe12ff811566620743f06
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/matvae_standard_norm.json
@@ -0,0 +1,76 @@
+{
+ "dry_run": false,
+ "standard_vae": true,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/matvae",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 850,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-4,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "kl_weight": 1.0,
+ "iwae_K": 50,
+
+ "alpha": 1.0,
+ "beta": 2.0,
+ "gamma": 1.0,
+
+ "normalization_type": "standard",
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05,
+ "use_learned_variances": false,
+ "use_additional_losses": true
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+ }
\ No newline at end of file
diff --git a/deps/vomp/configs/materials/material_vae/standard_vae_final.json b/deps/vomp/configs/materials/material_vae/standard_vae_final.json
new file mode 100644
index 0000000000000000000000000000000000000000..c8f296484d20ef37b083a23ea679cf2160e09a41
--- /dev/null
+++ b/deps/vomp/configs/materials/material_vae/standard_vae_final.json
@@ -0,0 +1,67 @@
+{
+ "dry_run": false,
+ "standard_vae": true,
+
+ "data_csv": "datasets/latent_space/materials_filtered.csv",
+ "dataloader": {
+ "batch_size": 256,
+ "num_workers": 8,
+ "pin_memory": true,
+ "prefetch_factor": 4,
+ "persistent_workers": true
+ },
+
+ "project_dir": "./outputs/standard_vae",
+ "tracker_name": "tb_logs",
+ "log_with": "tensorboard",
+
+ "epochs": 25000,
+ "gradient_accumulation_steps": 1,
+ "keep_last_checkpoints": 3,
+
+ "mixed_precision": "no",
+ "use_stateful_dataloader": false,
+ "find_unused_parameters": false,
+
+ "compile": {
+ "enabled": false,
+ "backend": "inductor",
+ "mode": "default",
+ "fullgraph": true,
+ "dynamic": false
+ },
+
+ "optimizer": {
+ "lr": 1e-3,
+ "weight_decay": 1e-4,
+ "grad_clip_norm": 5.0
+ },
+
+ "lr_scheduler": {
+ "type": "cosine",
+ "eta_min": 1e-5
+ },
+
+ "free_nats": 0.1,
+ "kl_annealing": true,
+ "kl_annealing_epochs": 200,
+ "recon_scale": 1.0,
+ "iwae_K": 50,
+
+ "eval_interval": 1,
+ "save_interval": 1,
+ "visualization_interval": 1000,
+ "n_vis_samples": 5,
+ "n_vis_steps": 10,
+
+ "model": {
+ "width": 256,
+ "depth": 3,
+ "z_dim": 2,
+ "p_drop": 0.05
+ },
+
+ "seed": 42,
+
+ "resume_from_checkpoint": null
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/armchair_and_orange.json b/deps/vomp/configs/sim/armchair_and_orange.json
new file mode 100644
index 0000000000000000000000000000000000000000..f688dd52c0033b9c3741fbdcccfaf033f0a0e910
--- /dev/null
+++ b/deps/vomp/configs/sim/armchair_and_orange.json
@@ -0,0 +1,59 @@
+{
+ "simulation": {
+ "dt": 0.01,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.005
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 2.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "armchair_1",
+ "type": "msh",
+ "msh_path": "assets/armchair/armchair.msh",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 0.004, 0.0],
+ "rotation": [270.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/armchair/materials_aligned.npz"
+ }
+ },
+ {
+ "name": "orange_1",
+ "type": "msh",
+ "msh_path": "assets/orange/orange_02_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 3.0,
+ "translation": [0.0, 2.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/armchair_and_orange",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/falling_armchair.json b/deps/vomp/configs/sim/falling_armchair.json
new file mode 100644
index 0000000000000000000000000000000000000000..5a18314a80f04566fb85a545e0752834cb8323c4
--- /dev/null
+++ b/deps/vomp/configs/sim/falling_armchair.json
@@ -0,0 +1,48 @@
+{
+ "simulation": {
+ "dt": 0.01,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.01
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 1.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "armchair_1",
+ "type": "msh",
+ "msh_path": "assets/armchair/armchair.msh",
+ "visual_mesh": "assets/armchair/armchair_inst_base.obj",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 1.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/armchair/materials_aligned.npz"
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/falling_armchair",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/falling_bar_stool.json b/deps/vomp/configs/sim/falling_bar_stool.json
new file mode 100644
index 0000000000000000000000000000000000000000..028b1628f5199617b62f9b104408e9fee3ddf19a
--- /dev/null
+++ b/deps/vomp/configs/sim/falling_bar_stool.json
@@ -0,0 +1,50 @@
+{
+ "simulation": {
+ "dt": 0.01,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.01
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 1.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "bar_stool_1",
+ "type": "msh",
+ "msh_path": "assets/bar_stool/bar_stool_inst_base.msh",
+ "visual_mesh": "assets/bar_stool/bar_stool_inst_base.obj",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 2.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "youngs_modulus": 1e5,
+ "density": 900.0,
+ "poisson_ratio": 0.3
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/falling_bar_stool",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/falling_birch.json b/deps/vomp/configs/sim/falling_birch.json
new file mode 100644
index 0000000000000000000000000000000000000000..51701c91d45fc9f77f9b0d051c3654c157f2da40
--- /dev/null
+++ b/deps/vomp/configs/sim/falling_birch.json
@@ -0,0 +1,50 @@
+{
+ "simulation": {
+ "dt": 0.005,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.01
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 1.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "birch_1",
+ "type": "msh",
+ "msh_path": "assets/birch/birch.msh",
+ "visual_mesh": "assets/birch/birch_lowbackseat_inst_base.obj",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 2.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "youngs_modulus": 1e6,
+ "density": 1000.0,
+ "poisson_ratio": 0.45
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/falling_birch",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/falling_oranges.json b/deps/vomp/configs/sim/falling_oranges.json
new file mode 100644
index 0000000000000000000000000000000000000000..bf6ed0a5e49b9ff4f790b9fd6f18b61f6450e317
--- /dev/null
+++ b/deps/vomp/configs/sim/falling_oranges.json
@@ -0,0 +1,80 @@
+{
+ "simulation": {
+ "dt": 0.02,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.01
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 1.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "orange_1",
+ "type": "voxel",
+ "voxel_path": "assets/orange/voxels.ply",
+ "visual_mesh": "assets/orange/orange_02_inst_base.obj",
+ "normalize_visual_mesh": true,
+ "voxel_size": 1.0,
+ "scale": 1.0,
+ "max_voxels": 32000,
+ "translation": [0.0, 2.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ },
+ {
+ "name": "orange_2",
+ "type": "voxel",
+ "voxel_path": "assets/orange/voxels.ply",
+ "visual_mesh": "assets/orange/orange_02_inst_base.obj",
+ "normalize_visual_mesh": true,
+ "voxel_size": 1.0,
+ "scale": 1.0,
+ "max_voxels": 32000,
+ "translation": [0.0, 3.5, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ },
+ {
+ "name": "orange_3",
+ "type": "voxel",
+ "voxel_path": "assets/orange/voxels.ply",
+ "visual_mesh": "assets/orange/orange_02_inst_base.obj",
+ "normalize_visual_mesh": true,
+ "voxel_size": 1.0,
+ "scale": 1.0,
+ "max_voxels": 32000,
+ "translation": [0.0, 5.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/falling_oranges",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/falling_sphere_soft.json b/deps/vomp/configs/sim/falling_sphere_soft.json
new file mode 100644
index 0000000000000000000000000000000000000000..ccb478b2de136f5b6eeb8ae95e8d0cabe20f3fc6
--- /dev/null
+++ b/deps/vomp/configs/sim/falling_sphere_soft.json
@@ -0,0 +1,51 @@
+{
+ "simulation": {
+ "dt": 0.005,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": false,
+ "d_hat": 0.005
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 0.01
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "soft_sphere",
+ "type": "msh",
+ "msh_path": "assets/sphere/sphere_tetrahedral.msh",
+ "visual_mesh": "assets/sphere/sphere_visual.obj",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 0.3, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "type": "StableNeoHookean",
+ "young_modulus": 1e4,
+ "poisson_ratio": 0.3,
+ "density": 1000
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/fem",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "info"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/zag_and_falling_orange.json b/deps/vomp/configs/sim/zag_and_falling_orange.json
new file mode 100644
index 0000000000000000000000000000000000000000..8429eb06d3e58f3d92622b45f46afac33f4b90f0
--- /dev/null
+++ b/deps/vomp/configs/sim/zag_and_falling_orange.json
@@ -0,0 +1,59 @@
+{
+ "simulation": {
+ "dt": 0.01,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.005
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 2.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "zag_middle_base",
+ "type": "msh",
+ "msh_path": "assets/zag/zag_middle_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 0.001, 0.0],
+ "rotation": [270.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/zag/materials.npz"
+ }
+ },
+ {
+ "name": "orange_1",
+ "type": "msh",
+ "msh_path": "assets/orange/orange_02_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 3.0,
+ "translation": [0.0, 2.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/zag_and_falling_orange",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/configs/sim/zag_and_falling_oranges.json b/deps/vomp/configs/sim/zag_and_falling_oranges.json
new file mode 100644
index 0000000000000000000000000000000000000000..816174ef2b9f1c5db855aaa4380ca5e053465272
--- /dev/null
+++ b/deps/vomp/configs/sim/zag_and_falling_oranges.json
@@ -0,0 +1,98 @@
+{
+ "simulation": {
+ "dt": 0.01,
+ "gravity": [0.0, -9.8, 0.0],
+ "max_frames": 500,
+ "auto_start": false,
+ "contact": {
+ "friction_enable": true,
+ "d_hat": 0.005
+ }
+ },
+ "engine": {
+ "type": "cuda"
+ },
+ "contact_model": {
+ "friction": 0.5,
+ "contact_resistance": 2.0
+ },
+ "ground": {
+ "enable": true,
+ "height": 0.0
+ },
+ "objects": [
+ {
+ "name": "zag_middle_base",
+ "type": "msh",
+ "msh_path": "assets/zag/zag_middle_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 1.0,
+ "translation": [0.0, 0.01, 0.0],
+ "rotation": [270.0, 0.0, 0.0],
+ "apply_boundary_conditions": true,
+ "boundary_fix_percentage": 0.15,
+ "material": {
+ "file": "assets/zag/materials.npz"
+ }
+ },
+
+ {
+ "name": "lemon_2",
+ "type": "msh",
+ "msh_path": "assets/orange/orange_02_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 2.0,
+ "translation": [0.15, 1.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ },
+ {
+ "name": "lemon_3",
+ "type": "msh",
+ "msh_path": "assets/orange/orange_02_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 2.0,
+ "translation": [-0.15, 1.0, 0.0],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ },
+ {
+ "name": "lemon_4",
+ "type": "msh",
+ "msh_path": "assets/orange/orange_02_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 2.0,
+ "translation": [0.0, 1.0, 0.15],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ },
+ {
+ "name": "lemon_5",
+ "type": "msh",
+ "msh_path": "assets/orange/orange_02_inst_base.msh",
+ "normalize_visual_mesh": false,
+ "scale": 2.0,
+ "translation": [0.0, 1.0, -0.15],
+ "rotation": [0.0, 0.0, 0.0],
+ "material": {
+ "file": "assets/orange/materials.npz"
+ }
+ }
+ ],
+ "gui": {
+ "enable": true
+ },
+ "output": {
+ "directory": "./outputs/simulation_output/zag_and_falling_oranges",
+ "save_meshes": true
+ },
+ "logging": {
+ "level": "warn"
+ }
+}
\ No newline at end of file
diff --git a/deps/vomp/dataset_toolkits/abo/ABO500.py b/deps/vomp/dataset_toolkits/abo/ABO500.py
new file mode 100644
index 0000000000000000000000000000000000000000..4fdc931a7d8c651a5b322b72deacb4d1ef2c3bae
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/abo/ABO500.py
@@ -0,0 +1,204 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import json
+import argparse
+import pandas as pd
+from concurrent.futures import ThreadPoolExecutor
+from tqdm import tqdm
+import hashlib
+
+
+def add_args(parser: argparse.ArgumentParser):
+ parser.add_argument(
+ "--abo_500_dir",
+ type=str,
+ default="/home/rdagli/code/datasets/abo_500",
+ help="Path to the ABO 500 dataset directory",
+ )
+ parser.add_argument(
+ "--abo_3d_dir",
+ type=str,
+ default="/home/rdagli/code/datasets/abo-3dmodels/3dmodels",
+ help="Path to the ABO 3D models directory",
+ )
+ parser.add_argument(
+ "--split",
+ type=str,
+ default="all",
+ choices=["train", "val", "test", "all"],
+ help="Which split to process",
+ )
+ parser.add_argument(
+ "--limit",
+ type=int,
+ default=None,
+ help="Limit to first N objects for testing",
+ )
+
+
+def get_file_hash(file_path):
+ """Get SHA256 hash of a file."""
+ hasher = hashlib.sha256()
+ with open(file_path, "rb") as f:
+ for chunk in iter(lambda: f.read(4096), b""):
+ hasher.update(chunk)
+ return hasher.hexdigest()
+
+
+def get_metadata(abo_500_dir, abo_3d_dir, split="all", limit=None, **kwargs):
+ """Get metadata for ABO 500 dataset."""
+ splits_path = os.path.join(abo_500_dir, "splits.json")
+
+ if not os.path.exists(splits_path):
+ raise FileNotFoundError(f"Splits file not found at {splits_path}")
+
+ with open(splits_path, "r") as f:
+ splits_data = json.load(f)
+
+ if split == "all":
+ object_ids = splits_data["train"] + splits_data["val"] + splits_data["test"]
+ else:
+ object_ids = splits_data[split]
+
+ # Apply limit if specified
+ if limit is not None:
+ object_ids = object_ids[:limit]
+
+ print(f"Processing {len(object_ids)} objects from {split} split")
+
+ # Create metadata records
+ metadata_records = []
+ missing_files = []
+
+ for object_id in tqdm(object_ids, desc="Building metadata"):
+ # Extract base ID (remove suffix after underscore if present)
+ base_id = object_id.split("_")[0]
+
+ # Search for GLB file - try multiple patterns and locations
+ glb_path = None
+
+ # Pattern 1: Try with base_id in the directory based on first character
+ first_char = base_id[0]
+ candidate_path = os.path.join(
+ abo_3d_dir, "original", first_char, f"{base_id}.glb"
+ )
+ if os.path.exists(candidate_path):
+ glb_path = candidate_path
+ else:
+ # Pattern 2: Try with full object_id (without underscore splitting)
+ first_char_full = object_id[0]
+ candidate_path = os.path.join(
+ abo_3d_dir, "original", first_char_full, f"{object_id}.glb"
+ )
+ if os.path.exists(candidate_path):
+ glb_path = candidate_path
+ else:
+ # Pattern 3: Search in all directories for the base_id
+ for dir_name in os.listdir(os.path.join(abo_3d_dir, "original")):
+ dir_path = os.path.join(abo_3d_dir, "original", dir_name)
+ if os.path.isdir(dir_path):
+ candidate_path = os.path.join(dir_path, f"{base_id}.glb")
+ if os.path.exists(candidate_path):
+ glb_path = candidate_path
+ break
+ # Also try the full object_id
+ candidate_path = os.path.join(dir_path, f"{object_id}.glb")
+ if os.path.exists(candidate_path):
+ glb_path = candidate_path
+ break
+
+ if glb_path and os.path.exists(glb_path):
+ # Get file hash
+ try:
+ sha256 = get_file_hash(glb_path)
+ metadata_records.append(
+ {
+ "object_id": object_id,
+ "sha256": sha256,
+ "local_path": glb_path,
+ "file_type": "glb",
+ "split": split,
+ "dataset": "ABO500",
+ }
+ )
+ except Exception as e:
+ print(f"Error processing {object_id}: {e}")
+ missing_files.append(object_id)
+ else:
+ print(
+ f"Warning: GLB file not found for {object_id} (tried base_id: {base_id})"
+ )
+ missing_files.append(object_id)
+
+ if missing_files:
+ print(f"Warning: {len(missing_files)} objects have missing GLB files")
+
+ metadata = pd.DataFrame(metadata_records)
+ return metadata
+
+
+def download(metadata, output_dir, **kwargs):
+ """For ABO 500, files are already downloaded, so just return local paths."""
+ download_records = []
+
+ for _, row in metadata.iterrows():
+ download_records.append(
+ {"sha256": row["sha256"], "local_path": row["local_path"]}
+ )
+
+ return pd.DataFrame(download_records)
+
+
+def foreach_instance(
+ metadata, output_dir, func, max_workers=None, desc="Processing objects"
+) -> pd.DataFrame:
+ """Process each instance in the metadata."""
+ import os
+ from concurrent.futures import ThreadPoolExecutor
+ from tqdm import tqdm
+
+ # Convert to list of records
+ metadata_records = metadata.to_dict("records")
+
+ # Processing objects
+ records = []
+ max_workers = max_workers or os.cpu_count()
+
+ try:
+ with (
+ ThreadPoolExecutor(max_workers=max_workers) as executor,
+ tqdm(total=len(metadata_records), desc=desc) as pbar,
+ ):
+
+ def worker(metadatum):
+ try:
+ local_path = metadatum["local_path"]
+ sha256 = metadatum["sha256"]
+ record = func(local_path, sha256)
+ if record is not None:
+ records.append(record)
+ pbar.update()
+ except Exception as e:
+ print(f"Error processing object {sha256}: {e}")
+ pbar.update()
+
+ executor.map(worker, metadata_records)
+ executor.shutdown(wait=True)
+ except Exception as e:
+ print(f"Error happened during processing: {e}")
+
+ return pd.DataFrame.from_records(records)
diff --git a/deps/vomp/dataset_toolkits/abo/build_metadata.py b/deps/vomp/dataset_toolkits/abo/build_metadata.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a216667b39b82fc7c02c006776b62dd97bf88a8
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/abo/build_metadata.py
@@ -0,0 +1,108 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import argparse
+import pandas as pd
+from easydict import EasyDict as edict
+
+# Add current directory to path to import dataset modules
+sys.path.insert(0, os.path.dirname(__file__))
+
+# Import the local ABO500 module directly
+import ABO500 as dataset_utils
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Build metadata for ABO 500 dataset")
+ parser.add_argument(
+ "--output_dir",
+ type=str,
+ required=True,
+ help="Directory to save the metadata and processed files",
+ )
+
+ # Add dataset-specific arguments
+ dataset_utils.add_args(parser)
+
+ args = parser.parse_args()
+ opt = edict(vars(args))
+
+ # Create output directory
+ os.makedirs(opt.output_dir, exist_ok=True)
+
+ # Get metadata
+ print("Building metadata for ABO 500 dataset...")
+ metadata = dataset_utils.get_metadata(**opt)
+
+ # Add default columns for tracking processing status
+ metadata["rendered"] = False
+ metadata["voxelized"] = False
+ metadata["feature_dinov2_vitl14_reg"] = False
+
+ # Check for existing processed files and update flags
+ for idx, row in metadata.iterrows():
+ sha256 = row["sha256"]
+
+ # Check if voxel file exists
+ voxel_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ if os.path.exists(voxel_path):
+ metadata.at[idx, "voxelized"] = True
+
+ # Check if render file exists (transforms.json)
+ render_path = os.path.join(opt.output_dir, "renders", sha256, "transforms.json")
+ if os.path.exists(render_path):
+ metadata.at[idx, "rendered"] = True
+
+ # Check if feature file exists
+ feature_path = os.path.join(
+ opt.output_dir, "features", "dinov2_vitl14_reg", f"{sha256}.npz"
+ )
+ if os.path.exists(feature_path):
+ metadata.at[idx, "feature_dinov2_vitl14_reg"] = True
+
+ # Save metadata
+ metadata_path = os.path.join(opt.output_dir, "metadata.csv")
+ metadata.to_csv(metadata_path, index=False)
+
+ print(f"Metadata saved to {metadata_path}")
+ print(f"Total objects: {len(metadata)}")
+ print(f"Objects by split:")
+ if "split" in metadata.columns:
+ print(metadata["split"].value_counts())
+
+ # Also save a summary file
+ summary = {
+ "total_objects": len(metadata),
+ "dataset": "ABO500",
+ "splits": (
+ metadata["split"].value_counts().to_dict()
+ if "split" in metadata.columns
+ else {}
+ ),
+ "output_dir": opt.output_dir,
+ }
+
+ import json
+
+ with open(os.path.join(opt.output_dir, "dataset_summary.json"), "w") as f:
+ json.dump(summary, f, indent=2)
+
+ print("Dataset summary saved to dataset_summary.json")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/dataset_toolkits/abo/extract_feature.py b/deps/vomp/dataset_toolkits/abo/extract_feature.py
new file mode 100644
index 0000000000000000000000000000000000000000..1174c4019e64296fcfd76241d30bf5284d249cec
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/abo/extract_feature.py
@@ -0,0 +1,381 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import copy
+import sys
+import json
+import argparse
+import torch
+import torch.nn.functional as F
+import numpy as np
+import pandas as pd
+import utils3d
+from tqdm import tqdm
+from easydict import EasyDict as edict
+from torchvision import transforms
+from PIL import Image
+
+# Add current directory to path to import dataset modules
+sys.path.insert(0, os.path.dirname(__file__))
+import ABO500 as dataset_utils
+
+torch.set_grad_enabled(False)
+
+
+def get_data(frames, sha256, output_dir):
+ """
+ Load and preprocess rendered images for feature extraction.
+
+ Args:
+ frames (list): List of frame data from transforms.json
+ sha256 (str): SHA256 hash of the object
+ output_dir (str): Output directory containing renders
+
+ Returns:
+ list: List of processed image data
+ """
+ valid_data = []
+
+ for view in frames:
+ image_path = os.path.join(output_dir, "renders", sha256, view["file_path"])
+ try:
+ # Check if file exists before trying to open it
+ if not os.path.exists(image_path):
+ print(f"Warning: Image file {image_path} not found, skipping")
+ continue
+
+ image = Image.open(image_path)
+ except Exception as e:
+ print(f"Error loading image {image_path}: {e}")
+ continue
+
+ try:
+ # Resize and normalize image
+ image = image.resize((518, 518), Image.Resampling.LANCZOS)
+ image = np.array(image).astype(np.float32) / 255
+ image = image[:, :, :3] * image[:, :, 3:] # Apply alpha channel
+ image = torch.from_numpy(image).permute(2, 0, 1).float()
+
+ # Extract camera parameters
+ c2w = torch.tensor(view["transform_matrix"])
+ c2w[:3, 1:3] *= -1
+ extrinsics = torch.inverse(c2w)
+ fov = view["camera_angle_x"]
+ intrinsics = utils3d.torch.intrinsics_from_fov_xy(
+ torch.tensor(fov), torch.tensor(fov)
+ )
+
+ valid_data.append(
+ {"image": image, "extrinsics": extrinsics, "intrinsics": intrinsics}
+ )
+ except Exception as e:
+ print(f"Error processing image {image_path}: {e}")
+ continue
+
+ if len(valid_data) == 0:
+ print(f"Warning: No valid images found for {sha256}")
+ else:
+ print(f"Loaded {len(valid_data)}/{len(frames)} valid images for {sha256}")
+
+ return valid_data
+
+
+def extract_features(
+ file_path,
+ sha256,
+ output_dir=None,
+ model=None,
+ transform=None,
+ batch_size=16,
+ feature_name="dinov2_vitl14_reg",
+):
+ """
+ Extract features for a single object.
+
+ Args:
+ file_path (str): Path to the GLB file (not used directly, but needed for interface)
+ sha256 (str): SHA256 hash of the object
+ output_dir (str): Output directory
+ model: Pre-loaded feature extraction model
+ transform: Image transformation pipeline
+ batch_size (int): Batch size for processing
+ feature_name (str): Name of the feature extraction method
+
+ Returns:
+ dict: Result dictionary with processing info
+ """
+ try:
+ # Load transforms.json
+ transforms_path = os.path.join(output_dir, "renders", sha256, "transforms.json")
+ if not os.path.exists(transforms_path):
+ print(f"transforms.json not found for {sha256}")
+ return {"sha256": sha256, f"feature_{feature_name}": False}
+
+ with open(transforms_path, "r") as f:
+ metadata_json = json.load(f)
+
+ frames = metadata_json["frames"]
+ data = get_data(frames, sha256, output_dir)
+
+ if len(data) == 0:
+ print(f"Skipping {sha256}: no valid image data")
+ return {"sha256": sha256, f"feature_{feature_name}": False}
+
+ # Apply transform to images
+ for datum in data:
+ datum["image"] = transform(datum["image"])
+
+ # Load voxel positions
+ voxel_path = os.path.join(output_dir, "voxels", f"{sha256}.ply")
+ if not os.path.exists(voxel_path):
+ print(f"Voxel file not found for {sha256}")
+ return {"sha256": sha256, f"feature_{feature_name}": False}
+
+ positions = utils3d.io.read_ply(voxel_path)[0]
+ positions = torch.from_numpy(positions).float().cuda()
+ indices = ((positions + 0.5) * 64).long()
+ # Clamp indices to valid range [0, 63] to handle floating point precision issues
+ indices = torch.clamp(indices, 0, 63)
+
+ n_views = len(data)
+ n_patch = 518 // 14
+ pack = {
+ "indices": indices.cpu().numpy().astype(np.uint8),
+ }
+
+ patchtokens_lst = []
+ uv_lst = []
+
+ # Process in batches
+ for i in range(0, n_views, batch_size):
+ batch_data = data[i : i + batch_size]
+ bs = len(batch_data)
+ batch_images = torch.stack([d["image"] for d in batch_data]).cuda()
+ batch_extrinsics = torch.stack([d["extrinsics"] for d in batch_data]).cuda()
+ batch_intrinsics = torch.stack([d["intrinsics"] for d in batch_data]).cuda()
+
+ # Extract features using the model
+ features = model(batch_images, is_training=True)
+
+ # Project 3D positions to 2D
+ uv = (
+ utils3d.torch.project_cv(positions, batch_extrinsics, batch_intrinsics)[
+ 0
+ ]
+ * 2
+ - 1
+ )
+
+ # Extract patch tokens
+ patchtokens = (
+ features["x_prenorm"][:, model.num_register_tokens + 1 :]
+ .permute(0, 2, 1)
+ .reshape(bs, 1024, n_patch, n_patch)
+ )
+ patchtokens_lst.append(patchtokens)
+ uv_lst.append(uv)
+
+ patchtokens = torch.cat(patchtokens_lst, dim=0)
+ uv = torch.cat(uv_lst, dim=0)
+
+ # Sample features at voxel positions
+ pack["patchtokens"] = (
+ F.grid_sample(
+ patchtokens,
+ uv.unsqueeze(1),
+ mode="bilinear",
+ align_corners=False,
+ )
+ .squeeze(2)
+ .permute(0, 2, 1)
+ .cpu()
+ .numpy()
+ )
+ pack["patchtokens"] = np.mean(pack["patchtokens"], axis=0).astype(np.float16)
+
+ # Save features
+ save_path = os.path.join(output_dir, "features", feature_name, f"{sha256}.npz")
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+ np.savez_compressed(save_path, **pack)
+
+ return {"sha256": sha256, f"feature_{feature_name}": True}
+
+ except Exception as e:
+ print(f"Error processing {sha256}: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return {"sha256": sha256, f"feature_{feature_name}": False}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Extract features for ABO 500 dataset")
+ parser.add_argument(
+ "--output_dir",
+ type=str,
+ required=True,
+ help="Directory containing metadata and where to save features",
+ )
+ parser.add_argument(
+ "--model",
+ type=str,
+ default="dinov2_vitl14_reg",
+ help="Feature extraction model",
+ )
+ parser.add_argument(
+ "--instances",
+ type=str,
+ default=None,
+ help="Specific instances to process (comma-separated or file path)",
+ )
+ parser.add_argument("--batch_size", type=int, default=16)
+ parser.add_argument("--rank", type=int, default=0)
+ parser.add_argument("--world_size", type=int, default=1)
+ parser.add_argument(
+ "--force",
+ action="store_true",
+ help="Force feature extraction even if already processed",
+ )
+ parser.add_argument(
+ "--limit", type=int, default=None, help="Process only the first N objects"
+ )
+
+ args = parser.parse_args()
+ opt = edict(vars(args))
+
+ feature_name = opt.model
+
+ # Create features directory
+ os.makedirs(os.path.join(opt.output_dir, "features", feature_name), exist_ok=True)
+
+ # Load model
+ print(f"Loading model: {opt.model}")
+ dinov2_model = torch.hub.load("facebookresearch/dinov2", opt.model)
+ dinov2_model.eval().cuda()
+ transform = transforms.Compose(
+ [
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
+ ]
+ )
+
+ # Load metadata
+ metadata_path = os.path.join(opt.output_dir, "metadata.csv")
+ if not os.path.exists(metadata_path):
+ raise ValueError(f"metadata.csv not found at {metadata_path}")
+
+ metadata = pd.read_csv(metadata_path)
+
+ # Filter instances if specified
+ if opt.instances is not None:
+ if os.path.exists(opt.instances):
+ with open(opt.instances, "r") as f:
+ instances = f.read().splitlines()
+ else:
+ instances = opt.instances.split(",")
+ metadata = metadata[metadata["sha256"].isin(instances)]
+ else:
+ # Only process objects that have been rendered and voxelized
+ if "rendered" in metadata.columns:
+ metadata = metadata[metadata["rendered"] == True]
+ if "voxelized" in metadata.columns:
+ metadata = metadata[metadata["voxelized"] == True]
+
+ # Only process objects that haven't had features extracted yet
+ if f"feature_{feature_name}" in metadata.columns and not opt.force:
+ metadata = metadata[metadata[f"feature_{feature_name}"] == False]
+
+ # Apply distributed processing
+ start = len(metadata) * opt.rank // opt.world_size
+ end = len(metadata) * (opt.rank + 1) // opt.world_size
+ metadata = metadata[start:end]
+
+ # Apply limit if specified
+ if opt.limit is not None:
+ metadata = metadata.head(opt.limit)
+
+ print(f"Processing {len(metadata)} objects...")
+
+ # Track already processed objects
+ records = []
+ sha256s = list(metadata["sha256"].values)
+
+ # Filter out objects that are already processed
+ if not opt.force:
+ for sha256 in copy.copy(sha256s):
+ feature_path = os.path.join(
+ opt.output_dir, "features", feature_name, f"{sha256}.npz"
+ )
+ if os.path.exists(feature_path):
+ records.append({"sha256": sha256, f"feature_{feature_name}": True})
+ sha256s.remove(sha256)
+
+ # Filter out objects that don't have required prerequisite files
+ initial_count = len(sha256s)
+ filtered_sha256s = []
+
+ for sha256 in sha256s:
+ # Check for voxel file
+ voxel_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ if not os.path.exists(voxel_path):
+ print(f"Skipping {sha256}: voxel file not found")
+ continue
+
+ # Check for transforms.json
+ transforms_path = os.path.join(
+ opt.output_dir, "renders", sha256, "transforms.json"
+ )
+ if not os.path.exists(transforms_path):
+ print(f"Skipping {sha256}: transforms.json not found")
+ continue
+
+ filtered_sha256s.append(sha256)
+
+ sha256s = filtered_sha256s
+ print(
+ f"Filtered from {initial_count} to {len(sha256s)} objects with required files"
+ )
+
+ # Extract features for remaining objects
+ if len(sha256s) > 0:
+ for sha256 in tqdm(sha256s, desc="Extracting features"):
+ # Get the file path (not used directly but needed for interface consistency)
+ file_path = metadata[metadata["sha256"] == sha256]["local_path"].iloc[0]
+
+ result = extract_features(
+ file_path=file_path,
+ sha256=sha256,
+ output_dir=opt.output_dir,
+ model=dinov2_model,
+ transform=transform,
+ batch_size=opt.batch_size,
+ feature_name=feature_name,
+ )
+
+ if result is not None:
+ records.append(result)
+
+ # Save results
+ if len(records) > 0:
+ results_df = pd.DataFrame.from_records(records)
+ results_df.to_csv(
+ os.path.join(opt.output_dir, f"feature_{feature_name}_{opt.rank}.csv"),
+ index=False,
+ )
+ print(
+ f"Feature extraction complete. Results saved to feature_{feature_name}_{opt.rank}.csv"
+ )
+ else:
+ print("No objects processed.")
diff --git a/deps/vomp/dataset_toolkits/abo/render.py b/deps/vomp/dataset_toolkits/abo/render.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3f935e3bbd1325983a8ba8ce68195225991d4d0
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/abo/render.py
@@ -0,0 +1,241 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import json
+import copy
+import sys
+import argparse
+import pandas as pd
+from easydict import EasyDict as edict
+from functools import partial
+from subprocess import DEVNULL, call
+import numpy as np
+
+# Add current directory to path to import dataset modules
+sys.path.insert(0, os.path.dirname(__file__))
+import ABO500 as dataset_utils
+
+# Import from the existing render.py utils
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from utils import sphere_hammersley_sequence
+
+BLENDER_LINK = (
+ "https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
+)
+BLENDER_INSTALLATION_PATH = "/tmp"
+BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
+
+
+def _install_blender():
+ """Install Blender if not already installed."""
+ if not os.path.exists(BLENDER_PATH):
+ os.system("sudo apt-get update")
+ os.system(
+ "sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
+ )
+ os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
+ os.system(
+ f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
+ )
+
+
+def _render_glb(file_path, sha256, output_dir, num_views):
+ """
+ Render a GLB file from multiple viewpoints.
+
+ Args:
+ file_path (str): Path to the GLB file
+ sha256 (str): SHA256 hash of the file
+ output_dir (str): Directory to save renders
+ num_views (int): Number of viewpoints to render
+
+ Returns:
+ dict: Result dictionary with rendering info
+ """
+ # Convert to absolute path to avoid issues with relative paths
+ output_dir = os.path.abspath(output_dir)
+ output_folder = os.path.join(output_dir, "renders", sha256)
+
+ # Build camera parameters {yaw, pitch, radius, fov}
+ yaws = []
+ pitchs = []
+ offset = (np.random.rand(), np.random.rand())
+ for i in range(num_views):
+ y, p = sphere_hammersley_sequence(i, num_views, offset)
+ yaws.append(y)
+ pitchs.append(p)
+
+ radius = [2] * num_views
+ fov = [40 / 180 * np.pi] * num_views
+ views = [
+ {"yaw": y, "pitch": p, "radius": r, "fov": f}
+ for y, p, r, f in zip(yaws, pitchs, radius, fov)
+ ]
+
+ # Construct Blender command
+ blender_script_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), "blender_script", "render.py"
+ )
+
+ args = [
+ BLENDER_PATH,
+ "-b",
+ "-P",
+ blender_script_path,
+ "--",
+ "--views",
+ json.dumps(views),
+ "--object",
+ os.path.expanduser(file_path),
+ "--resolution",
+ "512",
+ "--output_folder",
+ output_folder,
+ "--engine",
+ "CYCLES",
+ "--save_mesh",
+ ]
+
+ try:
+ # Execute Blender rendering
+ result = call(args, stdout=DEVNULL, stderr=DEVNULL)
+
+ # Check if rendering was successful
+ if result == 0 and os.path.exists(
+ os.path.join(output_folder, "transforms.json")
+ ):
+ return {"sha256": sha256, "rendered": True}
+ else:
+ print(f"Rendering failed for {sha256}")
+ return {"sha256": sha256, "rendered": False}
+
+ except Exception as e:
+ print(f"Error rendering {file_path}: {e}")
+ return {"sha256": sha256, "rendered": False}
+
+
+def _render(file_path, sha256, output_dir=None, num_views=150):
+ """Wrapper function for rendering."""
+ return _render_glb(file_path, sha256, output_dir, num_views)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Render ABO 500 dataset")
+ parser.add_argument(
+ "--output_dir",
+ type=str,
+ required=True,
+ help="Directory containing metadata and where to save renders",
+ )
+ parser.add_argument(
+ "--instances",
+ type=str,
+ default=None,
+ help="Specific instances to process (comma-separated or file path)",
+ )
+ parser.add_argument(
+ "--num_views", type=int, default=150, help="Number of views to render"
+ )
+ parser.add_argument(
+ "--force", action="store_true", help="Force rendering even if already processed"
+ )
+ parser.add_argument("--rank", type=int, default=0)
+ parser.add_argument("--world_size", type=int, default=1)
+ parser.add_argument("--max_workers", type=int, default=8)
+ parser.add_argument(
+ "--limit", type=int, default=None, help="Process only the first N objects"
+ )
+
+ args = parser.parse_args()
+ opt = edict(vars(args))
+
+ # Create renders directory
+ os.makedirs(os.path.join(opt.output_dir, "renders"), exist_ok=True)
+
+ # Install Blender if needed
+ print("Checking Blender installation...", flush=True)
+ _install_blender()
+
+ # Load metadata
+ metadata_path = os.path.join(opt.output_dir, "metadata.csv")
+ if not os.path.exists(metadata_path):
+ raise ValueError(f"metadata.csv not found at {metadata_path}")
+
+ metadata = pd.read_csv(metadata_path)
+
+ # Filter instances if specified
+ if opt.instances is not None:
+ if os.path.exists(opt.instances):
+ with open(opt.instances, "r") as f:
+ instances = f.read().splitlines()
+ else:
+ instances = opt.instances.split(",")
+ metadata = metadata[metadata["sha256"].isin(instances)]
+ else:
+ # Only process objects that have valid local paths
+ metadata = metadata[metadata["local_path"].notna()]
+
+ # Only process objects that haven't been rendered yet
+ if "rendered" in metadata.columns and not opt.force:
+ metadata = metadata[metadata["rendered"] == False]
+
+ # Apply distributed processing
+ start = len(metadata) * opt.rank // opt.world_size
+ end = len(metadata) * (opt.rank + 1) // opt.world_size
+ metadata = metadata[start:end]
+
+ # Apply limit if specified
+ if opt.limit is not None:
+ metadata = metadata.head(opt.limit)
+
+ print(f"Processing {len(metadata)} objects...")
+
+ # Track already processed objects
+ records = []
+
+ # Filter out objects that are already processed
+ if not opt.force:
+ for sha256 in copy.copy(metadata["sha256"].values):
+ transforms_path = os.path.join(
+ opt.output_dir, "renders", sha256, "transforms.json"
+ )
+ if os.path.exists(transforms_path):
+ records.append({"sha256": sha256, "rendered": True})
+ metadata = metadata[metadata["sha256"] != sha256]
+
+ # Process remaining objects
+ if len(metadata) > 0:
+ func = partial(_render, output_dir=opt.output_dir, num_views=opt.num_views)
+ rendered = dataset_utils.foreach_instance(
+ metadata,
+ opt.output_dir,
+ func,
+ max_workers=opt.max_workers,
+ desc="Rendering objects",
+ )
+
+ # Combine results
+ if len(records) > 0:
+ rendered = pd.concat([rendered, pd.DataFrame.from_records(records)])
+
+ # Save results
+ rendered.to_csv(
+ os.path.join(opt.output_dir, f"rendered_{opt.rank}.csv"), index=False
+ )
+
+ print(f"Rendering complete. Results saved to rendered_{opt.rank}.csv")
+ else:
+ print("No objects to process.")
diff --git a/deps/vomp/dataset_toolkits/abo/voxelize.py b/deps/vomp/dataset_toolkits/abo/voxelize.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3ae49d1ca32440cd7cf78916d97d657c0f62c8e
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/abo/voxelize.py
@@ -0,0 +1,306 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import copy
+import sys
+import argparse
+import pandas as pd
+from easydict import EasyDict as edict
+from functools import partial
+import numpy as np
+import open3d as o3d
+import utils3d
+import trimesh
+import tempfile
+import shutil
+
+# Add current directory to path to import dataset modules
+sys.path.insert(0, os.path.dirname(__file__))
+
+import ABO500 as dataset_utils
+
+
+def voxelize_mesh(
+ vertices, faces, voxel_size=1 / 64, center_scale=None, max_voxels=None
+):
+ """
+ Voxelize a mesh represented by vertices and faces using volumetric voxelization.
+
+ Args:
+ vertices (numpy.ndarray): Array of vertices
+ faces (numpy.ndarray): Array of faces
+ voxel_size (float): Size of each voxel
+ center_scale (tuple): Optional center and scale for normalization
+ max_voxels (int): Maximum number of voxels to return (will subsample if exceeded)
+
+ Returns:
+ tuple: (voxel_centers, voxel_grid) - center coordinates of voxels and Trimesh voxel grid
+ """
+ # Create a Trimesh mesh
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
+
+ # Normalize the mesh to [-0.5, 0.5] range
+ vertices = mesh.vertices.copy()
+
+ if center_scale is None:
+ vertices_min = np.min(vertices, axis=0)
+ vertices_max = np.max(vertices, axis=0)
+ center = (vertices_min + vertices_max) / 2
+ scale = np.max(vertices_max - vertices_min)
+ else:
+ center, scale = center_scale
+
+ vertices = (vertices - center) / scale
+ vertices = np.clip(vertices, -0.5 + 1e-6, 0.5 - 1e-6)
+
+ # Update mesh with normalized vertices
+ mesh.vertices = vertices
+
+ # Create volumetric voxel grid using Trimesh
+ voxel_grid = mesh.voxelized(pitch=voxel_size).fill()
+
+ # Get voxel centers from the filled voxel grid
+ voxel_centers = voxel_grid.points
+
+ # Subsample if we have too many voxels
+ if max_voxels is not None and len(voxel_centers) > max_voxels:
+ print(f"Subsampling voxels: {len(voxel_centers):,} -> {max_voxels:,}")
+ # Use random sampling to maintain spatial distribution
+ np.random.seed(42) # For reproducibility
+ indices = np.random.choice(len(voxel_centers), max_voxels, replace=False)
+ voxel_centers = voxel_centers[indices]
+
+ return voxel_centers, voxel_grid
+
+
+def load_glb_mesh(glb_path):
+ """
+ Load a GLB file and extract mesh data.
+
+ Args:
+ glb_path (str): Path to the GLB file
+
+ Returns:
+ tuple: (vertices, faces) - mesh vertices and faces
+ """
+ try:
+ # Load the GLB file using trimesh
+ mesh = trimesh.load(glb_path)
+
+ # Handle different mesh types
+ if isinstance(mesh, trimesh.Scene):
+ # If it's a scene, combine all meshes
+ combined_mesh = trimesh.util.concatenate(
+ [
+ geometry
+ for geometry in mesh.geometry.values()
+ if isinstance(geometry, trimesh.Trimesh)
+ ]
+ )
+ if combined_mesh is None:
+ raise ValueError("No valid meshes found in GLB file")
+ mesh = combined_mesh
+ elif not isinstance(mesh, trimesh.Trimesh):
+ raise ValueError("GLB file does not contain a valid mesh")
+
+ # Ensure the mesh has faces
+ if len(mesh.faces) == 0:
+ raise ValueError("Mesh has no faces")
+
+ return mesh.vertices, mesh.faces
+
+ except Exception as e:
+ print(f"Error loading GLB file {glb_path}: {e}")
+ return None, None
+
+
+def voxelize_glb(glb_path, sha256, output_dir, max_voxels=None):
+ """
+ Voxelize a GLB file and save the result.
+
+ Args:
+ glb_path (str): Path to the GLB file
+ sha256 (str): SHA256 hash of the file
+ output_dir (str): Directory to save the voxelized data
+ max_voxels (int): Maximum number of voxels to generate
+
+ Returns:
+ dict: Result dictionary with processing info
+ """
+ try:
+ # Load the GLB mesh
+ vertices, faces = load_glb_mesh(glb_path)
+
+ if vertices is None or faces is None:
+ print(f"Failed to load mesh from {glb_path}")
+ return {"sha256": sha256, "voxelized": False, "num_voxels": 0}
+
+ print(f"Loaded mesh with {len(vertices)} vertices and {len(faces)} faces")
+
+ # Voxelize the mesh
+ voxel_centers, voxel_grid = voxelize_mesh(
+ vertices, faces, max_voxels=max_voxels
+ )
+
+ if len(voxel_centers) == 0:
+ print(f"No voxels generated for {sha256}")
+ return {"sha256": sha256, "voxelized": False, "num_voxels": 0}
+
+ # Save voxel centers as PLY file
+ ply_output_path = os.path.join(output_dir, "voxels", f"{sha256}.ply")
+ save_ply(ply_output_path, voxel_centers)
+
+ print(f"Voxelized {sha256}: {len(voxel_centers)} voxels")
+
+ return {"sha256": sha256, "voxelized": True, "num_voxels": len(voxel_centers)}
+
+ except Exception as e:
+ print(f"Error voxelizing {glb_path}: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return {"sha256": sha256, "voxelized": False, "num_voxels": 0}
+
+
+def save_ply(filename, points):
+ """
+ Save points as a PLY file.
+
+ Args:
+ filename (str): Output filename
+ points (numpy.ndarray): Array of 3D points
+ """
+ os.makedirs(os.path.dirname(filename), exist_ok=True)
+ pcd = o3d.geometry.PointCloud()
+ pcd.points = o3d.utility.Vector3dVector(points)
+ o3d.io.write_point_cloud(filename, pcd)
+
+
+def _voxelize(file_path, sha256, output_dir=None, max_voxels=None):
+ """Wrapper function for voxelization."""
+ return voxelize_glb(file_path, sha256, output_dir, max_voxels=max_voxels)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Voxelize ABO 500 dataset")
+ parser.add_argument(
+ "--output_dir",
+ type=str,
+ required=True,
+ help="Directory containing metadata and where to save voxelized data",
+ )
+ parser.add_argument(
+ "--instances",
+ type=str,
+ default=None,
+ help="Specific instances to process (comma-separated or file path)",
+ )
+ parser.add_argument(
+ "--force",
+ action="store_true",
+ help="Force voxelization even if already processed",
+ )
+ parser.add_argument("--rank", type=int, default=0)
+ parser.add_argument("--world_size", type=int, default=1)
+ parser.add_argument("--max_workers", type=int, default=None)
+ parser.add_argument(
+ "--limit", type=int, default=None, help="Process only the first N objects"
+ )
+ parser.add_argument(
+ "--max_voxels",
+ type=int,
+ default=70000,
+ help="Maximum number of voxels per asset",
+ )
+
+ args = parser.parse_args()
+ opt = edict(vars(args))
+
+ # Create voxels directory
+ os.makedirs(os.path.join(opt.output_dir, "voxels"), exist_ok=True)
+
+ # Load metadata
+ metadata_path = os.path.join(opt.output_dir, "metadata.csv")
+ if not os.path.exists(metadata_path):
+ raise ValueError(f"metadata.csv not found at {metadata_path}")
+
+ metadata = pd.read_csv(metadata_path)
+
+ # Filter instances if specified
+ if opt.instances is not None:
+ if os.path.exists(opt.instances):
+ with open(opt.instances, "r") as f:
+ instances = f.read().splitlines()
+ else:
+ instances = opt.instances.split(",")
+ metadata = metadata[metadata["sha256"].isin(instances)]
+ else:
+ # Only process objects that haven't been voxelized yet
+ if "voxelized" in metadata.columns and not opt.force:
+ metadata = metadata[metadata["voxelized"] == False]
+
+ # Apply distributed processing
+ start = len(metadata) * opt.rank // opt.world_size
+ end = len(metadata) * (opt.rank + 1) // opt.world_size
+ metadata = metadata[start:end]
+
+ # Apply limit if specified
+ if opt.limit is not None:
+ metadata = metadata.head(opt.limit)
+
+ print(f"Processing {len(metadata)} objects with max_voxels={opt.max_voxels:,}...")
+
+ # Track already processed objects
+ records = []
+
+ # Filter out objects that are already processed
+ if not opt.force:
+ for sha256 in copy.copy(metadata["sha256"].values):
+ ply_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ if os.path.exists(ply_path):
+ try:
+ pts = utils3d.io.read_ply(ply_path)[0]
+ records.append(
+ {"sha256": sha256, "voxelized": True, "num_voxels": len(pts)}
+ )
+ metadata = metadata[metadata["sha256"] != sha256]
+ except:
+ # If file is corrupted, re-process it
+ pass
+
+ # Process remaining objects
+ if len(metadata) > 0:
+ func = partial(_voxelize, output_dir=opt.output_dir, max_voxels=opt.max_voxels)
+ voxelized = dataset_utils.foreach_instance(
+ metadata,
+ opt.output_dir,
+ func,
+ max_workers=opt.max_workers,
+ desc="Voxelizing",
+ )
+
+ # Combine results
+ if len(records) > 0:
+ voxelized = pd.concat([voxelized, pd.DataFrame.from_records(records)])
+
+ # Save results
+ voxelized.to_csv(
+ os.path.join(opt.output_dir, f"voxelized_{opt.rank}.csv"), index=False
+ )
+
+ print(f"Voxelization complete. Results saved to voxelized_{opt.rank}.csv")
+ else:
+ print("No objects to process.")
diff --git a/deps/vomp/dataset_toolkits/blender_script/render.py b/deps/vomp/dataset_toolkits/blender_script/render.py
new file mode 100644
index 0000000000000000000000000000000000000000..2146d5c4eb87ed6c4056a7cd4e1428b38ed8e257
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/blender_script/render.py
@@ -0,0 +1,695 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse, sys, os, math, re, glob
+from typing import *
+import bpy
+from mathutils import Vector, Matrix
+import numpy as np
+import json
+import glob
+
+"""=============== BLENDER ==============="""
+
+IMPORT_FUNCTIONS: Dict[str, Callable] = {
+ "obj": bpy.ops.import_scene.obj,
+ "glb": bpy.ops.import_scene.gltf,
+ "gltf": bpy.ops.import_scene.gltf,
+ "usd": bpy.ops.import_scene.usd,
+ "fbx": bpy.ops.import_scene.fbx,
+ "stl": bpy.ops.import_mesh.stl,
+ "usda": bpy.ops.import_scene.usda,
+ "dae": bpy.ops.wm.collada_import,
+ "ply": bpy.ops.import_mesh.ply,
+ "abc": bpy.ops.wm.alembic_import,
+ "blend": bpy.ops.wm.append,
+}
+
+EXT = {
+ "PNG": "png",
+ "JPEG": "jpg",
+ "OPEN_EXR": "exr",
+ "TIFF": "tiff",
+ "BMP": "bmp",
+ "HDR": "hdr",
+ "TARGA": "tga",
+}
+
+
+def setup_gpu_devices():
+ """Setup GPU devices for Cycles rendering using the same approach as viz_fields.py."""
+ try:
+ cycles_prefs = bpy.context.preferences.addons["cycles"].preferences
+ except KeyError:
+ print("[ERROR] Cycles addon not found or not enabled")
+ return False
+
+ # Get available device types and try them in order of preference
+ available_types = cycles_prefs.get_device_types(bpy.context)
+ available_type_names = [dt[0] for dt in available_types]
+
+ # Try device types in order of preference (same as viz_fields.py)
+ preferred_types = ["OPTIX", "CUDA", "HIP", "ONEAPI", "OPENCL", "METAL"]
+ available_preferred = [t for t in preferred_types if t in available_type_names]
+
+ best_type = None
+ for device_type in available_preferred:
+ try:
+ cycles_prefs.compute_device_type = device_type
+ cycles_prefs.get_devices()
+
+ gpu_devices = [
+ dev for dev in cycles_prefs.devices if dev.type == device_type
+ ]
+
+ if gpu_devices:
+ best_type = device_type
+ print(f"[INFO] โ Using compute device type: {device_type}")
+ break
+
+ except Exception as e:
+ print(f"[WARNING] โ Could not set {device_type}: {e}")
+ continue
+
+ if not best_type:
+ print("[ERROR] โ No GPU compute device type available")
+ return False
+
+ # Enable all GPU devices for the selected type
+ gpu_devices_enabled = 0
+ for device in cycles_prefs.devices:
+ if device.type == best_type:
+ device.use = True
+ gpu_devices_enabled += 1
+ print(f"[INFO] โ Enabled GPU device: {device.name}")
+
+ print(
+ f"[INFO] GPU setup complete: {gpu_devices_enabled} device(s) enabled with {best_type}"
+ )
+ return gpu_devices_enabled > 0
+
+
+def init_render(
+ engine="CYCLES", resolution=512, geo_mode=False, use_gpu=True, gpu_device="OPTIX"
+):
+ bpy.context.scene.render.engine = engine
+ bpy.context.scene.render.resolution_x = resolution
+ bpy.context.scene.render.resolution_y = resolution
+ bpy.context.scene.render.resolution_percentage = 100
+ bpy.context.scene.render.image_settings.file_format = "PNG"
+ bpy.context.scene.render.image_settings.color_mode = "RGBA"
+ bpy.context.scene.render.film_transparent = True
+
+ # Enhanced GPU setup using the same approach as viz_fields.py
+ if use_gpu:
+ gpu_success = setup_gpu_devices()
+ if gpu_success:
+ bpy.context.scene.cycles.device = "GPU"
+ print("[INFO] โ
GPU rendering enabled")
+ else:
+ bpy.context.scene.cycles.device = "CPU"
+ print("[WARNING] โ GPU setup failed, using CPU rendering")
+ else:
+ bpy.context.scene.cycles.device = "CPU"
+ print("[INFO] CPU rendering requested")
+
+ bpy.context.scene.cycles.samples = 128 if not geo_mode else 1
+ bpy.context.scene.cycles.filter_type = "BOX"
+ bpy.context.scene.cycles.filter_width = 1
+ bpy.context.scene.cycles.diffuse_bounces = 1
+ bpy.context.scene.cycles.glossy_bounces = 1
+ bpy.context.scene.cycles.transparent_max_bounces = 3 if not geo_mode else 0
+ bpy.context.scene.cycles.transmission_bounces = 3 if not geo_mode else 1
+ bpy.context.scene.cycles.use_denoising = True
+
+
+def init_nodes(save_depth=False, save_normal=False, save_albedo=False, save_mist=False):
+ if not any([save_depth, save_normal, save_albedo, save_mist]):
+ return {}, {}
+ outputs = {}
+ spec_nodes = {}
+
+ bpy.context.scene.use_nodes = True
+ bpy.context.scene.view_layers["View Layer"].use_pass_z = save_depth
+ bpy.context.scene.view_layers["View Layer"].use_pass_normal = save_normal
+ bpy.context.scene.view_layers["View Layer"].use_pass_diffuse_color = save_albedo
+ bpy.context.scene.view_layers["View Layer"].use_pass_mist = save_mist
+
+ nodes = bpy.context.scene.node_tree.nodes
+ links = bpy.context.scene.node_tree.links
+ for n in nodes:
+ nodes.remove(n)
+
+ render_layers = nodes.new("CompositorNodeRLayers")
+
+ if save_depth:
+ depth_file_output = nodes.new("CompositorNodeOutputFile")
+ depth_file_output.base_path = ""
+ depth_file_output.file_slots[0].use_node_format = True
+ depth_file_output.format.file_format = "PNG"
+ depth_file_output.format.color_depth = "16"
+ depth_file_output.format.color_mode = "BW"
+ # Remap to 0-1
+ map = nodes.new(type="CompositorNodeMapRange")
+ map.inputs[1].default_value = 0 # (min value you will be getting)
+ map.inputs[2].default_value = 10 # (max value you will be getting)
+ map.inputs[3].default_value = 0 # (min value you will map to)
+ map.inputs[4].default_value = 1 # (max value you will map to)
+
+ links.new(render_layers.outputs["Depth"], map.inputs[0])
+ links.new(map.outputs[0], depth_file_output.inputs[0])
+
+ outputs["depth"] = depth_file_output
+ spec_nodes["depth_map"] = map
+
+ if save_normal:
+ normal_file_output = nodes.new("CompositorNodeOutputFile")
+ normal_file_output.base_path = ""
+ normal_file_output.file_slots[0].use_node_format = True
+ normal_file_output.format.file_format = "OPEN_EXR"
+ normal_file_output.format.color_mode = "RGB"
+ normal_file_output.format.color_depth = "16"
+
+ links.new(render_layers.outputs["Normal"], normal_file_output.inputs[0])
+
+ outputs["normal"] = normal_file_output
+
+ if save_albedo:
+ albedo_file_output = nodes.new("CompositorNodeOutputFile")
+ albedo_file_output.base_path = ""
+ albedo_file_output.file_slots[0].use_node_format = True
+ albedo_file_output.format.file_format = "PNG"
+ albedo_file_output.format.color_mode = "RGBA"
+ albedo_file_output.format.color_depth = "8"
+
+ alpha_albedo = nodes.new("CompositorNodeSetAlpha")
+
+ links.new(render_layers.outputs["DiffCol"], alpha_albedo.inputs["Image"])
+ links.new(render_layers.outputs["Alpha"], alpha_albedo.inputs["Alpha"])
+ links.new(alpha_albedo.outputs["Image"], albedo_file_output.inputs[0])
+
+ outputs["albedo"] = albedo_file_output
+
+ if save_mist:
+ bpy.data.worlds["World"].mist_settings.start = 0
+ bpy.data.worlds["World"].mist_settings.depth = 10
+
+ mist_file_output = nodes.new("CompositorNodeOutputFile")
+ mist_file_output.base_path = ""
+ mist_file_output.file_slots[0].use_node_format = True
+ mist_file_output.format.file_format = "PNG"
+ mist_file_output.format.color_mode = "BW"
+ mist_file_output.format.color_depth = "16"
+
+ links.new(render_layers.outputs["Mist"], mist_file_output.inputs[0])
+
+ outputs["mist"] = mist_file_output
+
+ return outputs, spec_nodes
+
+
+def init_scene() -> None:
+ """Resets the scene to a clean state.
+
+ Returns:
+ None
+ """
+ # delete everything
+ for obj in bpy.data.objects:
+ bpy.data.objects.remove(obj, do_unlink=True)
+
+ # delete all the materials
+ for material in bpy.data.materials:
+ bpy.data.materials.remove(material, do_unlink=True)
+
+ # delete all the textures
+ for texture in bpy.data.textures:
+ bpy.data.textures.remove(texture, do_unlink=True)
+
+ # delete all the images
+ for image in bpy.data.images:
+ bpy.data.images.remove(image, do_unlink=True)
+
+
+def init_camera():
+ cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
+ bpy.context.collection.objects.link(cam)
+ bpy.context.scene.camera = cam
+ cam.data.sensor_height = cam.data.sensor_width = 32
+ cam_constraint = cam.constraints.new(type="TRACK_TO")
+ cam_constraint.track_axis = "TRACK_NEGATIVE_Z"
+ cam_constraint.up_axis = "UP_Y"
+ cam_empty = bpy.data.objects.new("Empty", None)
+ cam_empty.location = (0, 0, 0)
+ bpy.context.scene.collection.objects.link(cam_empty)
+ cam_constraint.target = cam_empty
+ return cam
+
+
+def init_lighting():
+ # Clear existing lights
+ bpy.ops.object.select_all(action="DESELECT")
+ bpy.ops.object.select_by_type(type="LIGHT")
+ bpy.ops.object.delete()
+
+ # Create key light
+ default_light = bpy.data.objects.new(
+ "Default_Light", bpy.data.lights.new("Default_Light", type="POINT")
+ )
+ bpy.context.collection.objects.link(default_light)
+ default_light.data.energy = 1000
+ default_light.location = (4, 1, 6)
+ default_light.rotation_euler = (0, 0, 0)
+
+ # create top light
+ top_light = bpy.data.objects.new(
+ "Top_Light", bpy.data.lights.new("Top_Light", type="AREA")
+ )
+ bpy.context.collection.objects.link(top_light)
+ top_light.data.energy = 10000
+ top_light.location = (0, 0, 10)
+ top_light.scale = (100, 100, 100)
+
+ # create bottom light
+ bottom_light = bpy.data.objects.new(
+ "Bottom_Light", bpy.data.lights.new("Bottom_Light", type="AREA")
+ )
+ bpy.context.collection.objects.link(bottom_light)
+ bottom_light.data.energy = 1000
+ bottom_light.location = (0, 0, -10)
+ bottom_light.rotation_euler = (0, 0, 0)
+
+ return {
+ "default_light": default_light,
+ "top_light": top_light,
+ "bottom_light": bottom_light,
+ }
+
+
+def load_object(object_path: str) -> None:
+ """Loads a model with a supported file extension into the scene.
+
+ Args:
+ object_path (str): Path to the model file.
+
+ Raises:
+ ValueError: If the file extension is not supported.
+
+ Returns:
+ None
+ """
+ file_extension = object_path.split(".")[-1].lower()
+ if file_extension is None:
+ raise ValueError(f"Unsupported file type: {object_path}")
+
+ if file_extension == "usdz":
+ # install usdz io package
+ dirname = os.path.dirname(os.path.realpath(__file__))
+ usdz_package = os.path.join(dirname, "io_scene_usdz.zip")
+ bpy.ops.preferences.addon_install(filepath=usdz_package)
+ # enable it
+ addon_name = "io_scene_usdz"
+ bpy.ops.preferences.addon_enable(module=addon_name)
+ # import the usdz
+ from io_scene_usdz.import_usdz import import_usdz
+
+ import_usdz(context, filepath=object_path, materials=True, animations=True)
+ return None
+
+ # load from existing import functions
+ import_function = IMPORT_FUNCTIONS[file_extension]
+
+ print(f"Loading object from {object_path}")
+ if file_extension == "blend":
+ import_function(directory=object_path, link=False)
+ elif file_extension in {"glb", "gltf"}:
+ import_function(
+ filepath=object_path, merge_vertices=True, import_shading="NORMALS"
+ )
+ else:
+ import_function(filepath=object_path)
+
+
+def delete_invisible_objects() -> None:
+ """Deletes all invisible objects in the scene.
+
+ Returns:
+ None
+ """
+ # bpy.ops.object.mode_set(mode="OBJECT")
+ bpy.ops.object.select_all(action="DESELECT")
+ for obj in bpy.context.scene.objects:
+ if obj.hide_viewport or obj.hide_render:
+ obj.hide_viewport = False
+ obj.hide_render = False
+ obj.hide_select = False
+ obj.select_set(True)
+ bpy.ops.object.delete()
+
+ # Delete invisible collections
+ invisible_collections = [col for col in bpy.data.collections if col.hide_viewport]
+ for col in invisible_collections:
+ bpy.data.collections.remove(col)
+
+
+def split_mesh_normal():
+ bpy.ops.object.select_all(action="DESELECT")
+ objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"]
+ bpy.context.view_layer.objects.active = objs[0]
+ for obj in objs:
+ obj.select_set(True)
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_all(action="SELECT")
+ bpy.ops.mesh.split_normals()
+ bpy.ops.object.mode_set(mode="OBJECT")
+ bpy.ops.object.select_all(action="DESELECT")
+
+
+def delete_custom_normals():
+ for this_obj in bpy.data.objects:
+ if this_obj.type == "MESH":
+ bpy.context.view_layer.objects.active = this_obj
+ bpy.ops.mesh.customdata_custom_splitnormals_clear()
+
+
+def override_material():
+ new_mat = bpy.data.materials.new(name="Override0123456789")
+ new_mat.use_nodes = True
+ new_mat.node_tree.nodes.clear()
+ bsdf = new_mat.node_tree.nodes.new("ShaderNodeBsdfDiffuse")
+ bsdf.inputs[0].default_value = (0.5, 0.5, 0.5, 1)
+ bsdf.inputs[1].default_value = 1
+ output = new_mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
+ new_mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
+ bpy.context.scene.view_layers["View Layer"].material_override = new_mat
+
+
+def unhide_all_objects() -> None:
+ """Unhides all objects in the scene.
+
+ Returns:
+ None
+ """
+ for obj in bpy.context.scene.objects:
+ obj.hide_set(False)
+
+
+def convert_to_meshes() -> None:
+ """Converts all objects in the scene to meshes.
+
+ Returns:
+ None
+ """
+ bpy.ops.object.select_all(action="DESELECT")
+ bpy.context.view_layer.objects.active = [
+ obj for obj in bpy.context.scene.objects if obj.type == "MESH"
+ ][0]
+ for obj in bpy.context.scene.objects:
+ obj.select_set(True)
+ bpy.ops.object.convert(target="MESH")
+
+
+def triangulate_meshes() -> None:
+ """Triangulates all meshes in the scene.
+
+ Returns:
+ None
+ """
+ bpy.ops.object.select_all(action="DESELECT")
+ objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"]
+ bpy.context.view_layer.objects.active = objs[0]
+ for obj in objs:
+ obj.select_set(True)
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.reveal()
+ bpy.ops.mesh.select_all(action="SELECT")
+ bpy.ops.mesh.quads_convert_to_tris(quad_method="BEAUTY", ngon_method="BEAUTY")
+ bpy.ops.object.mode_set(mode="OBJECT")
+ bpy.ops.object.select_all(action="DESELECT")
+
+
+def scene_bbox() -> Tuple[Vector, Vector]:
+ """Returns the bounding box of the scene.
+
+ Taken from Shap-E rendering script
+ (https://github.com/openai/shap-e/blob/main/shap_e/rendering/blender/blender_script.py#L68-L82)
+
+ Returns:
+ Tuple[Vector, Vector]: The minimum and maximum coordinates of the bounding box.
+ """
+ bbox_min = (math.inf,) * 3
+ bbox_max = (-math.inf,) * 3
+ found = False
+ scene_meshes = [
+ obj
+ for obj in bpy.context.scene.objects.values()
+ if isinstance(obj.data, bpy.types.Mesh)
+ ]
+ for obj in scene_meshes:
+ found = True
+ for coord in obj.bound_box:
+ coord = Vector(coord)
+ coord = obj.matrix_world @ coord
+ bbox_min = tuple(min(x, y) for x, y in zip(bbox_min, coord))
+ bbox_max = tuple(max(x, y) for x, y in zip(bbox_max, coord))
+ if not found:
+ raise RuntimeError("no objects in scene to compute bounding box for")
+ return Vector(bbox_min), Vector(bbox_max)
+
+
+def normalize_scene() -> Tuple[float, Vector]:
+ """Normalizes the scene by scaling and translating it to fit in a unit cube centered
+ at the origin.
+
+ Mostly taken from the Point-E / Shap-E rendering script
+ (https://github.com/openai/point-e/blob/main/point_e/evals/scripts/blender_script.py#L97-L112),
+ but fix for multiple root objects: (see bug report here:
+ https://github.com/openai/shap-e/pull/60).
+
+ Returns:
+ Tuple[float, Vector]: The scale factor and the offset applied to the scene.
+ """
+ scene_root_objects = [
+ obj for obj in bpy.context.scene.objects.values() if not obj.parent
+ ]
+ if len(scene_root_objects) > 1:
+ # create an empty object to be used as a parent for all root objects
+ scene = bpy.data.objects.new("ParentEmpty", None)
+ bpy.context.scene.collection.objects.link(scene)
+
+ # parent all root objects to the empty object
+ for obj in scene_root_objects:
+ obj.parent = scene
+ else:
+ scene = scene_root_objects[0]
+
+ bbox_min, bbox_max = scene_bbox()
+ scale = 1 / max(bbox_max - bbox_min)
+ scene.scale = scene.scale * scale
+
+ # Apply scale to matrix_world.
+ bpy.context.view_layer.update()
+ bbox_min, bbox_max = scene_bbox()
+ offset = -(bbox_min + bbox_max) / 2
+ scene.matrix_world.translation += offset
+ bpy.ops.object.select_all(action="DESELECT")
+
+ return scale, offset
+
+
+def get_transform_matrix(obj: bpy.types.Object) -> list:
+ pos, rt, _ = obj.matrix_world.decompose()
+ rt = rt.to_matrix()
+ matrix = []
+ for ii in range(3):
+ a = []
+ for jj in range(3):
+ a.append(rt[ii][jj])
+ a.append(pos[ii])
+ matrix.append(a)
+ matrix.append([0, 0, 0, 1])
+ return matrix
+
+
+def main(arg):
+ os.makedirs(arg.output_folder, exist_ok=True)
+
+ # Initialize context
+ init_render(
+ engine=arg.engine,
+ resolution=arg.resolution,
+ geo_mode=arg.geo_mode,
+ use_gpu=getattr(arg, "use_gpu", True),
+ gpu_device=getattr(arg, "gpu_device", "OPTIX"),
+ )
+ outputs, spec_nodes = init_nodes(
+ save_depth=arg.save_depth,
+ save_normal=arg.save_normal,
+ save_albedo=arg.save_albedo,
+ save_mist=arg.save_mist,
+ )
+ if arg.object.endswith(".blend"):
+ delete_invisible_objects()
+ else:
+ init_scene()
+ load_object(arg.object)
+ if arg.split_normal:
+ split_mesh_normal()
+ # delete_custom_normals()
+ print("[INFO] Scene initialized.")
+
+ # normalize scene
+ scale, offset = normalize_scene()
+ print("[INFO] Scene normalized.")
+
+ # Initialize camera and lighting
+ cam = init_camera()
+ init_lighting()
+ print("[INFO] Camera and lighting initialized.")
+
+ # Override material
+ if arg.geo_mode:
+ override_material()
+
+ # Create a list of views
+ to_export = {
+ "aabb": [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
+ "scale": scale,
+ "offset": [offset.x, offset.y, offset.z],
+ "frames": [],
+ }
+ views = json.loads(arg.views)
+ for i, view in enumerate(views):
+ cam.location = (
+ view["radius"] * np.cos(view["yaw"]) * np.cos(view["pitch"]),
+ view["radius"] * np.sin(view["yaw"]) * np.cos(view["pitch"]),
+ view["radius"] * np.sin(view["pitch"]),
+ )
+ cam.data.lens = 16 / np.tan(view["fov"] / 2)
+
+ if arg.save_depth:
+ spec_nodes["depth_map"].inputs[1].default_value = view[
+ "radius"
+ ] - 0.5 * np.sqrt(3)
+ spec_nodes["depth_map"].inputs[2].default_value = view[
+ "radius"
+ ] + 0.5 * np.sqrt(3)
+
+ bpy.context.scene.render.filepath = os.path.join(
+ arg.output_folder, f"{i:03d}.png"
+ )
+ for name, output in outputs.items():
+ output.file_slots[0].path = os.path.join(
+ arg.output_folder, f"{i:03d}_{name}"
+ )
+
+ # Render the scene
+ bpy.ops.render.render(write_still=True)
+ bpy.context.view_layer.update()
+ for name, output in outputs.items():
+ ext = EXT[output.format.file_format]
+ path = glob.glob(f"{output.file_slots[0].path}*.{ext}")[0]
+ os.rename(path, f"{output.file_slots[0].path}.{ext}")
+
+ # Save camera parameters
+ metadata = {
+ "file_path": f"{i:03d}.png",
+ "camera_angle_x": view["fov"],
+ "transform_matrix": get_transform_matrix(cam),
+ }
+ if arg.save_depth:
+ metadata["depth"] = {
+ "min": view["radius"] - 0.5 * np.sqrt(3),
+ "max": view["radius"] + 0.5 * np.sqrt(3),
+ }
+ to_export["frames"].append(metadata)
+
+ # Save the camera parameters
+ with open(os.path.join(arg.output_folder, "transforms.json"), "w") as f:
+ json.dump(to_export, f, indent=4)
+
+ if arg.save_mesh:
+ # triangulate meshes
+ unhide_all_objects()
+ convert_to_meshes()
+ triangulate_meshes()
+ print("[INFO] Meshes triangulated.")
+
+ # export ply mesh
+ bpy.ops.export_mesh.ply(filepath=os.path.join(arg.output_folder, "mesh.ply"))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Renders given obj file by rotation a camera around it."
+ )
+ parser.add_argument(
+ "--views",
+ type=str,
+ help="JSON string of views. Contains a list of {yaw, pitch, radius, fov} object.",
+ )
+ parser.add_argument(
+ "--object", type=str, help="Path to the 3D model file to be rendered."
+ )
+ parser.add_argument(
+ "--output_folder",
+ type=str,
+ default="/tmp",
+ help="The path the output will be dumped to.",
+ )
+ parser.add_argument(
+ "--resolution", type=int, default=512, help="Resolution of the images."
+ )
+ parser.add_argument(
+ "--engine",
+ type=str,
+ default="CYCLES",
+ help="Blender internal engine for rendering. E.g. CYCLES, BLENDER_EEVEE, ...",
+ )
+ parser.add_argument(
+ "--geo_mode", action="store_true", help="Geometry mode for rendering."
+ )
+ parser.add_argument(
+ "--save_depth", action="store_true", help="Save the depth maps."
+ )
+ parser.add_argument(
+ "--save_normal", action="store_true", help="Save the normal maps."
+ )
+ parser.add_argument(
+ "--save_albedo", action="store_true", help="Save the albedo maps."
+ )
+ parser.add_argument(
+ "--save_mist", action="store_true", help="Save the mist distance maps."
+ )
+ parser.add_argument(
+ "--split_normal", action="store_true", help="Split the normals of the mesh."
+ )
+ parser.add_argument(
+ "--save_mesh", action="store_true", help="Save the mesh as a .ply file."
+ )
+ parser.add_argument(
+ "--use_gpu", action="store_true", help="Use GPU acceleration for rendering."
+ )
+ parser.add_argument(
+ "--gpu_device",
+ type=str,
+ default="OPTIX",
+ choices=["OPTIX", "CUDA", "OPENCL"],
+ help="GPU device type for rendering (OPTIX, CUDA, or OPENCL).",
+ )
+ argv = sys.argv[sys.argv.index("--") + 1 :]
+ args = parser.parse_args(argv)
+
+ main(args)
diff --git a/deps/vomp/dataset_toolkits/build_metadata.py b/deps/vomp/dataset_toolkits/build_metadata.py
new file mode 100644
index 0000000000000000000000000000000000000000..37ad95fe928c31e5a8a82a1b2d4613d2809c1d88
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/build_metadata.py
@@ -0,0 +1,551 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+import sys
+import time
+import importlib
+import argparse
+import numpy as np
+import pandas as pd
+from tqdm import tqdm
+from easydict import EasyDict as edict
+from concurrent.futures import ThreadPoolExecutor
+import utils3d
+
+
+def get_first_directory(path):
+ with os.scandir(path) as it:
+ for entry in it:
+ if entry.is_dir():
+ return entry.name
+ return None
+
+
+def need_process(key):
+ return key in opt.field or opt.field == ["all"]
+
+
+if __name__ == "__main__":
+ dataset_utils = importlib.import_module(f"dataset_toolkits.datasets.{sys.argv[1]}")
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--output_dir", type=str, required=True, help="Directory to save the metadata"
+ )
+ parser.add_argument(
+ "--field",
+ type=str,
+ default="all",
+ help="Fields to process, separated by commas",
+ )
+ parser.add_argument(
+ "--from_file",
+ action="store_true",
+ help="Build metadata from file instead of from records of processings."
+ + "Useful when some processing fail to generate records but file already exists.",
+ )
+ parser.add_argument(
+ "--force_update_class_split",
+ action="store_true",
+ help="Force updating class and split information even if metadata file exists",
+ )
+ parser.add_argument(
+ "--skip_class_split_on_error",
+ action="store_true",
+ help="Skip updating class and split if an error occurs, instead of failing",
+ )
+ dataset_utils.add_args(parser)
+ opt = parser.parse_args(sys.argv[2:])
+ opt = edict(vars(opt))
+
+ os.makedirs(opt.output_dir, exist_ok=True)
+ os.makedirs(os.path.join(opt.output_dir, "merged_records"), exist_ok=True)
+
+ opt.field = opt.field.split(",")
+
+ timestamp = str(int(time.time()))
+
+ # Check if metadata file exists
+ metadata_exists = os.path.exists(os.path.join(opt.output_dir, "metadata.csv"))
+
+ # Load or create metadata
+ if metadata_exists:
+ print("Loading previous metadata...")
+ metadata = pd.read_csv(os.path.join(opt.output_dir, "metadata.csv"))
+
+ # Check if class and split information needs to be updated
+ requires_class_update = (
+ "class" not in metadata.columns or opt.force_update_class_split
+ )
+ requires_split_update = (
+ "split" not in metadata.columns or opt.force_update_class_split
+ )
+
+ if requires_class_update or requires_split_update:
+ # Generate fresh metadata with class and split information
+ print("Updating class and split information...")
+ try:
+ fresh_metadata = dataset_utils.get_metadata(**opt)
+
+ # Set index on sha256 for both DataFrames
+ metadata.set_index("sha256", inplace=True)
+ fresh_metadata.set_index("sha256", inplace=True)
+
+ # Update class information if needed
+ if requires_class_update and "class" in fresh_metadata.columns:
+ if "class" not in metadata.columns:
+ metadata["class"] = "unknown"
+ metadata.update(fresh_metadata[["class"]])
+
+ # Update split information if needed
+ if requires_split_update and "split" in fresh_metadata.columns:
+ if "split" not in metadata.columns:
+ metadata["split"] = "train" # Default value
+ metadata.update(fresh_metadata[["split"]])
+ except Exception as e:
+ if opt.skip_class_split_on_error:
+ print(f"Warning: Error updating class and split information: {e}")
+ print("Continuing with existing metadata...")
+ if "class" not in metadata.columns:
+ metadata["class"] = "unknown"
+ if "split" not in metadata.columns:
+ metadata["split"] = "train"
+ metadata.set_index("sha256", inplace=True)
+ else:
+ raise e
+ else:
+ metadata.set_index("sha256", inplace=True)
+ else:
+ # Create new metadata with all required information
+ print("Creating new metadata...")
+ try:
+ metadata = dataset_utils.get_metadata(**opt)
+ metadata.set_index("sha256", inplace=True)
+ except Exception as e:
+ if opt.skip_class_split_on_error:
+ print(
+ f"Warning: Error creating metadata with class and split information: {e}"
+ )
+ print("Creating basic metadata without class and split information...")
+ metadata = dataset_utils.get_metadata(skip_split=True, **opt)
+ metadata.set_index("sha256", inplace=True)
+ if "class" not in metadata.columns:
+ metadata["class"] = "unknown"
+ if "split" not in metadata.columns:
+ metadata["split"] = "train"
+ else:
+ raise e
+
+ # merge downloaded
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith("downloaded_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ if "local_path" in metadata.columns:
+ metadata.update(df, overwrite=True)
+ else:
+ metadata = metadata.join(df, on="sha256", how="left")
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # detect models
+ image_models = []
+ if os.path.exists(os.path.join(opt.output_dir, "features")):
+ image_models = os.listdir(os.path.join(opt.output_dir, "features"))
+ latent_models = []
+ if os.path.exists(os.path.join(opt.output_dir, "latents")):
+ latent_models = os.listdir(os.path.join(opt.output_dir, "latents"))
+ ss_latent_models = []
+ if os.path.exists(os.path.join(opt.output_dir, "ss_latents")):
+ ss_latent_models = os.listdir(os.path.join(opt.output_dir, "ss_latents"))
+ print(f"Image models: {image_models}")
+ print(f"Latent models: {latent_models}")
+ print(f"Sparse Structure latent models: {ss_latent_models}")
+
+ if "rendered" not in metadata.columns:
+ metadata["rendered"] = [False] * len(metadata)
+ if "voxelized" not in metadata.columns:
+ metadata["voxelized"] = [False] * len(metadata)
+ if "num_voxels" not in metadata.columns:
+ metadata["num_voxels"] = [0] * len(metadata)
+ if "cond_rendered" not in metadata.columns:
+ metadata["cond_rendered"] = [False] * len(metadata)
+ for model in image_models:
+ if f"feature_{model}" not in metadata.columns:
+ metadata[f"feature_{model}"] = [False] * len(metadata)
+ for model in latent_models:
+ if f"latent_{model}" not in metadata.columns:
+ metadata[f"latent_{model}"] = [False] * len(metadata)
+ for model in ss_latent_models:
+ if f"ss_latent_{model}" not in metadata.columns:
+ metadata[f"ss_latent_{model}"] = [False] * len(metadata)
+
+ # merge rendered
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith("rendered_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ metadata.update(df, overwrite=True)
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # merge voxelized
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith("voxelized_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ metadata.update(df, overwrite=True)
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # merge cond_rendered
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith("cond_rendered_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ metadata.update(df, overwrite=True)
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # merge features
+ for model in image_models:
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith(f"feature_{model}_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ metadata.update(df, overwrite=True)
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # merge latents
+ for model in latent_models:
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith(f"latent_{model}_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ metadata.update(df, overwrite=True)
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # merge sparse structure latents
+ for model in ss_latent_models:
+ df_files = [
+ f
+ for f in os.listdir(opt.output_dir)
+ if f.startswith(f"ss_latent_{model}_") and f.endswith(".csv")
+ ]
+ df_parts = []
+ for f in df_files:
+ try:
+ df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
+ except:
+ pass
+ if len(df_parts) > 0:
+ df = pd.concat(df_parts)
+ df.set_index("sha256", inplace=True)
+ metadata.update(df, overwrite=True)
+ for f in df_files:
+ shutil.move(
+ os.path.join(opt.output_dir, f),
+ os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
+ )
+
+ # build metadata from files
+ if opt.from_file:
+ with (
+ ThreadPoolExecutor(max_workers=os.cpu_count()) as executor,
+ tqdm(total=len(metadata), desc="Building metadata") as pbar,
+ ):
+
+ def worker(sha256):
+ try:
+ if (
+ need_process("rendered")
+ and metadata.loc[sha256, "rendered"] == False
+ and os.path.exists(
+ os.path.join(
+ opt.output_dir, "renders", sha256, "transforms.json"
+ )
+ )
+ ):
+ metadata.loc[sha256, "rendered"] = True
+ if (
+ need_process("voxelized")
+ and metadata.loc[sha256, "rendered"] == True
+ and metadata.loc[sha256, "voxelized"] == False
+ and os.path.exists(
+ os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ )
+ ):
+ try:
+ pts = utils3d.io.read_ply(
+ os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ )[0]
+ metadata.loc[sha256, "voxelized"] = True
+ metadata.loc[sha256, "num_voxels"] = len(pts)
+ except Exception as e:
+ pass
+ if (
+ need_process("cond_rendered")
+ and metadata.loc[sha256, "cond_rendered"] == False
+ and os.path.exists(
+ os.path.join(
+ opt.output_dir,
+ "renders_cond",
+ sha256,
+ "transforms.json",
+ )
+ )
+ ):
+ metadata.loc[sha256, "cond_rendered"] = True
+ for model in image_models:
+ if (
+ need_process(f"feature_{model}")
+ and metadata.loc[sha256, f"feature_{model}"] == False
+ and metadata.loc[sha256, "rendered"] == True
+ and metadata.loc[sha256, "voxelized"] == True
+ and os.path.exists(
+ os.path.join(
+ opt.output_dir, "features", model, f"{sha256}.npz"
+ )
+ )
+ ):
+ metadata.loc[sha256, f"feature_{model}"] = True
+ for model in latent_models:
+ if (
+ need_process(f"latent_{model}")
+ and metadata.loc[sha256, f"latent_{model}"] == False
+ and metadata.loc[sha256, "rendered"] == True
+ and metadata.loc[sha256, "voxelized"] == True
+ and os.path.exists(
+ os.path.join(
+ opt.output_dir, "latents", model, f"{sha256}.npz"
+ )
+ )
+ ):
+ metadata.loc[sha256, f"latent_{model}"] = True
+ for model in ss_latent_models:
+ if (
+ need_process(f"ss_latent_{model}")
+ and metadata.loc[sha256, f"ss_latent_{model}"] == False
+ and metadata.loc[sha256, "voxelized"] == True
+ and os.path.exists(
+ os.path.join(
+ opt.output_dir, "ss_latents", model, f"{sha256}.npz"
+ )
+ )
+ ):
+ metadata.loc[sha256, f"ss_latent_{model}"] = True
+ pbar.update()
+ except Exception as e:
+ print(f"Error processing {sha256}: {e}")
+ pbar.update()
+
+ executor.map(worker, metadata.index)
+ executor.shutdown(wait=True)
+
+ # Save dataset splits if we have split information
+ if "split" in metadata.columns:
+ os.makedirs(os.path.join(opt.output_dir, "splits"), exist_ok=True)
+ # Reset index to include sha256 in the exported files
+ metadata_export = metadata.reset_index()
+ for split in ["train", "val", "test"]:
+ split_df = metadata_export[metadata_export["split"] == split]
+ if not split_df.empty:
+ split_df.to_csv(
+ os.path.join(opt.output_dir, "splits", f"{split}.csv"), index=False
+ )
+
+ # statistics
+ metadata.to_csv(os.path.join(opt.output_dir, "metadata.csv"))
+ num_downloaded = (
+ metadata["local_path"].count() if "local_path" in metadata.columns else 0
+ )
+
+ # If from_file is True, update metadata to reflect actual files on disk before writing statistics
+ if opt.from_file:
+ print("Updating metadata to reflect actual files on disk...")
+ for model in image_models:
+ for sha256 in metadata.index:
+ actual_exists = os.path.exists(
+ os.path.join(opt.output_dir, "features", model, f"{sha256}.npz")
+ )
+ metadata.loc[sha256, f"feature_{model}"] = actual_exists
+
+ for model in latent_models:
+ for sha256 in metadata.index:
+ actual_exists = os.path.exists(
+ os.path.join(opt.output_dir, "latents", model, f"{sha256}.npz")
+ )
+ metadata.loc[sha256, f"latent_{model}"] = actual_exists
+
+ for model in ss_latent_models:
+ for sha256 in metadata.index:
+ actual_exists = os.path.exists(
+ os.path.join(opt.output_dir, "ss_latents", model, f"{sha256}.npz")
+ )
+ metadata.loc[sha256, f"ss_latent_{model}"] = actual_exists
+
+ # Save updated metadata
+ metadata.to_csv(os.path.join(opt.output_dir, "metadata.csv"))
+
+ with open(os.path.join(opt.output_dir, "statistics.txt"), "w") as f:
+ f.write("Statistics:\n")
+ f.write(f" - Number of assets: {len(metadata)}\n")
+ f.write(f" - Number of assets downloaded: {num_downloaded}\n")
+ f.write(f' - Number of assets rendered: {metadata["rendered"].sum()}\n')
+ f.write(f' - Number of assets voxelized: {metadata["voxelized"].sum()}\n')
+ if len(image_models) != 0:
+ f.write(f" - Number of assets with image features extracted:\n")
+ for model in image_models:
+ # Always use metadata counts since they're now accurate when from_file=True
+ f.write(f' - {model}: {metadata[f"feature_{model}"].sum()}\n')
+ if len(latent_models) != 0:
+ f.write(f" - Number of assets with latents extracted:\n")
+ for model in latent_models:
+ f.write(f' - {model}: {metadata[f"latent_{model}"].sum()}\n')
+ if len(ss_latent_models) != 0:
+ f.write(f" - Number of assets with sparse structure latents extracted:\n")
+ for model in ss_latent_models:
+ f.write(f' - {model}: {metadata[f"ss_latent_{model}"].sum()}\n')
+
+ # Only report captions if the column exists (it may not for Gaussian splats)
+ if "captions" in metadata.columns:
+ f.write(
+ f' - Number of assets with captions: {metadata["captions"].count()}\n'
+ )
+ else:
+ f.write(
+ f" - Number of assets with captions: N/A (no caption data available)\n"
+ )
+
+ f.write(
+ f' - Number of assets with image conditions: {metadata["cond_rendered"].sum()}\n'
+ )
+
+ # Add class distribution statistics
+ if "class" in metadata.columns:
+ f.write("\nClass distribution:\n")
+ class_counts = metadata["class"].value_counts()
+ for class_name, count in class_counts.items():
+ f.write(f" - {class_name}: {count} ({count/len(metadata)*100:.1f}%)\n")
+
+ # Add split statistics if split column exists
+ if "split" in metadata.columns:
+ f.write("\nDataset splits:\n")
+ split_counts = metadata["split"].value_counts()
+ for split_name, count in split_counts.items():
+ f.write(f" - {split_name}: {count} ({count/len(metadata)*100:.1f}%)\n")
+
+ # Add class distribution per split if both columns exist
+ if "class" in metadata.columns:
+ f.write("\nClass distribution per split:\n")
+ # Reset index to allow cross-tabulation
+ metadata_reset = metadata.reset_index()
+ # For each split, show class distribution
+ for split_name in ["train", "val", "test"]:
+ if split_name in split_counts:
+ f.write(f" {split_name.upper()}:\n")
+ split_data = metadata_reset[
+ metadata_reset["split"] == split_name
+ ]
+ class_in_split = split_data["class"].value_counts()
+ for class_name, count in class_in_split.items():
+ f.write(
+ f" - {class_name}: {count} ({count/len(split_data)*100:.1f}%)\n"
+ )
+
+ with open(os.path.join(opt.output_dir, "statistics.txt"), "r") as f:
+ print(f.read())
diff --git a/deps/vomp/dataset_toolkits/datasets/ABO.py b/deps/vomp/dataset_toolkits/datasets/ABO.py
new file mode 100644
index 0000000000000000000000000000000000000000..407893efc5007aa570b9b3dd6b09266aab9805d6
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/datasets/ABO.py
@@ -0,0 +1,132 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+import argparse
+import tarfile
+from concurrent.futures import ThreadPoolExecutor
+from tqdm import tqdm
+import pandas as pd
+from utils import get_file_hash
+
+
+def add_args(parser: argparse.ArgumentParser):
+ pass
+
+
+def get_metadata(**kwargs):
+ metadata = pd.read_csv("hf://datasets/JeffreyXiang/TRELLIS-500K/ABO.csv")
+ return metadata
+
+
+def download(metadata, output_dir, **kwargs):
+ os.makedirs(os.path.join(output_dir, "raw"), exist_ok=True)
+
+ if not os.path.exists(os.path.join(output_dir, "raw", "abo-3dmodels.tar")):
+ try:
+ os.makedirs(os.path.join(output_dir, "raw"), exist_ok=True)
+ os.system(
+ f"wget -O {output_dir}/raw/abo-3dmodels.tar https://amazon-berkeley-objects.s3.amazonaws.com/archives/abo-3dmodels.tar"
+ )
+ except:
+ print("\033[93m")
+ print(
+ "Error downloading ABO dataset. Please check your internet connection and try again."
+ )
+ print(
+ "Or, you can manually download the abo-3dmodels.tar file and place it in the {output_dir}/raw directory"
+ )
+ print(
+ "Visit https://amazon-berkeley-objects.s3.amazonaws.com/index.html for more information"
+ )
+ print("\033[0m")
+ raise FileNotFoundError("Error downloading ABO dataset")
+
+ downloaded = {}
+ metadata = metadata.set_index("file_identifier")
+ with tarfile.open(os.path.join(output_dir, "raw", "abo-3dmodels.tar")) as tar:
+ with (
+ ThreadPoolExecutor(max_workers=1) as executor,
+ tqdm(total=len(metadata), desc="Extracting") as pbar,
+ ):
+
+ def worker(instance: str) -> str:
+ try:
+ tar.extract(
+ f"3dmodels/original/{instance}",
+ path=os.path.join(output_dir, "raw"),
+ )
+ sha256 = get_file_hash(
+ os.path.join(output_dir, "raw/3dmodels/original", instance)
+ )
+ pbar.update()
+ return sha256
+ except Exception as e:
+ pbar.update()
+ print(f"Error extracting for {instance}: {e}")
+ return None
+
+ sha256s = executor.map(worker, metadata.index)
+ executor.shutdown(wait=True)
+
+ for k, sha256 in zip(metadata.index, sha256s):
+ if sha256 is not None:
+ if sha256 == metadata.loc[k, "sha256"]:
+ downloaded[sha256] = os.path.join("raw/3dmodels/original", k)
+ else:
+ print(f"Error downloading {k}: sha256s do not match")
+
+ return pd.DataFrame(downloaded.items(), columns=["sha256", "local_path"])
+
+
+def foreach_instance(
+ metadata, output_dir, func, max_workers=None, desc="Processing objects"
+) -> pd.DataFrame:
+ import os
+ from concurrent.futures import ThreadPoolExecutor
+ from tqdm import tqdm
+
+ # load metadata
+ metadata = metadata.to_dict("records")
+
+ # processing objects
+ records = []
+ max_workers = max_workers or os.cpu_count()
+ try:
+ with (
+ ThreadPoolExecutor(max_workers=max_workers) as executor,
+ tqdm(total=len(metadata), desc=desc) as pbar,
+ ):
+
+ def worker(metadatum):
+ try:
+ local_path = metadatum["local_path"]
+ sha256 = metadatum["sha256"]
+ file = os.path.join(output_dir, local_path)
+ record = func(file, sha256)
+ if record is not None:
+ records.append(record)
+ pbar.update()
+ except Exception as e:
+ print(f"Error processing object {sha256}: {e}")
+ pbar.update()
+
+ executor.map(worker, metadata)
+ executor.shutdown(wait=True)
+ except:
+ print("Error happened during processing.")
+
+ return pd.DataFrame.from_records(records)
diff --git a/deps/vomp/dataset_toolkits/datasets/__init__.py b/deps/vomp/dataset_toolkits/datasets/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5fcd7fab2c8148a7bd3eb6a8aa2105e1a8f0e6a5
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/datasets/__init__.py
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Dataset modules for TRELLIS preprocessing
diff --git a/deps/vomp/dataset_toolkits/datasets/allmats.py b/deps/vomp/dataset_toolkits/datasets/allmats.py
new file mode 100644
index 0000000000000000000000000000000000000000..f4267535eb526c798a64cb7a3ffb6d1a34854bc5
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/datasets/allmats.py
@@ -0,0 +1,510 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import json
+import pandas as pd
+import numpy as np
+import hashlib
+import random
+from glob import glob
+from sklearn.model_selection import train_test_split
+from typing import Dict, List, Optional, Any, Union
+
+
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ SIMREADY_PROPS_DIR,
+ COMMERCIAL_BASE_DIR,
+ RESIDENTIAL_BASE_DIR,
+ VEGETATION_BASE_DIR,
+ SIMREADY_ASSET_CLASS_MAPPING,
+ SIMREADY_ASSET_INFO_PATH,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets import (
+ simready,
+ commercial,
+ vegetation,
+ residential,
+ common,
+)
+
+
+def set_seeds(seed=42):
+ random.seed(seed)
+ np.random.seed(seed)
+
+
+def add_args(parser):
+ parser.add_argument(
+ "--simready_dir",
+ type=str,
+ default=SIMREADY_PROPS_DIR,
+ help="Path to the SimReady props directory",
+ )
+ parser.add_argument(
+ "--commercial_dir",
+ type=str,
+ default=COMMERCIAL_BASE_DIR,
+ help="Path to the Commercial models directory",
+ )
+ parser.add_argument(
+ "--residential_dir",
+ type=str,
+ default=RESIDENTIAL_BASE_DIR,
+ help="Path to the Residential models directory",
+ )
+ parser.add_argument(
+ "--vegetation_dir",
+ type=str,
+ default=VEGETATION_BASE_DIR,
+ help="Path to the Vegetation models directory",
+ )
+ parser.add_argument(
+ "--asset_info_path",
+ type=str,
+ default=SIMREADY_ASSET_INFO_PATH,
+ help="Path to the SimReady asset_info.json file",
+ )
+ parser.add_argument(
+ "--seed",
+ type=int,
+ default=42,
+ help="Random seed for reproducibility",
+ )
+ parser.add_argument(
+ "--default_class",
+ type=str,
+ default="unknown",
+ help="Default class label to use when class information is not available",
+ )
+ parser.add_argument(
+ "--include_datasets",
+ type=str,
+ default="simready,commercial,residential,vegetation",
+ help="Comma-separated list of datasets to include",
+ )
+
+
+def split_dataset(metadata, seed=42):
+
+ np.random.seed(seed)
+ random.seed(seed)
+
+ metadata_copy = metadata.copy()
+
+ metadata_copy["split"] = "train"
+
+ classes = metadata_copy["class"].unique()
+
+ large_classes = []
+ small_classes = []
+
+ for cls in classes:
+
+ cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
+ if len(cls_indices) >= 10:
+ large_classes.append(cls)
+ else:
+ small_classes.append(cls)
+
+ for cls in large_classes:
+ cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
+ random.shuffle(cls_indices)
+
+ n_samples = len(cls_indices)
+ n_train = int(0.8 * n_samples)
+ n_val = int(0.1 * n_samples)
+
+ train_indices = cls_indices[:n_train]
+ val_indices = cls_indices[n_train : n_train + n_val]
+ test_indices = cls_indices[n_train + n_val :]
+
+ metadata_copy.loc[train_indices, "split"] = "train"
+ metadata_copy.loc[val_indices, "split"] = "val"
+ metadata_copy.loc[test_indices, "split"] = "test"
+
+ total_samples = len(metadata_copy)
+ goal_train = int(0.8 * total_samples)
+ goal_val = int(0.1 * total_samples)
+ goal_test = total_samples - goal_train - goal_val
+
+ current_train = (metadata_copy["split"] == "train").sum()
+ current_val = (metadata_copy["split"] == "val").sum()
+ current_test = (metadata_copy["split"] == "test").sum()
+
+ small_indices = []
+ for cls in small_classes:
+ cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
+ small_indices.extend(cls_indices)
+
+ random.shuffle(small_indices)
+
+ need_train = max(0, goal_train - current_train)
+ need_val = max(0, goal_val - current_val)
+ need_test = max(0, goal_test - current_test)
+
+ idx = 0
+ while idx < len(small_indices):
+ if need_train > 0:
+ metadata_copy.loc[small_indices[idx], "split"] = "train"
+ need_train -= 1
+ idx += 1
+ elif need_val > 0:
+ metadata_copy.loc[small_indices[idx], "split"] = "val"
+ need_val -= 1
+ idx += 1
+ elif need_test > 0:
+ metadata_copy.loc[small_indices[idx], "split"] = "test"
+ need_test -= 1
+ idx += 1
+ else:
+
+ metadata_copy.loc[small_indices[idx:], "split"] = "train"
+ break
+
+ train_count = (metadata_copy["split"] == "train").sum()
+ val_count = (metadata_copy["split"] == "val").sum()
+ test_count = (metadata_copy["split"] == "test").sum()
+
+ print(
+ f"Dataset split: Train: {train_count} ({train_count/len(metadata_copy)*100:.1f}%), "
+ f"Val: {val_count} ({val_count/len(metadata_copy)*100:.1f}%), "
+ f"Test: {test_count} ({test_count/len(metadata_copy)*100:.1f}%)"
+ )
+
+ if small_classes:
+ print("\nSmall class distribution across splits:")
+ for cls in small_classes:
+ cls_data = metadata_copy[metadata_copy["class"] == cls]
+ cls_train = (cls_data["split"] == "train").sum()
+ cls_val = (cls_data["split"] == "val").sum()
+ cls_test = (cls_data["split"] == "test").sum()
+ cls_total = len(cls_data)
+ print(
+ f" - {cls} (total {cls_total}): Train: {cls_train}, Val: {cls_val}, Test: {cls_test}"
+ )
+
+ return metadata_copy
+
+
+def get_simready_metadata(simready_dir, asset_info_path, default_class="unknown"):
+
+ asset_class_mapping = SIMREADY_ASSET_CLASS_MAPPING
+
+ if not asset_class_mapping and asset_info_path and os.path.exists(asset_info_path):
+ try:
+ with open(asset_info_path, "r") as f:
+ asset_info = json.load(f)
+
+ asset_class_mapping = {}
+ for asset in asset_info:
+ simple_name = asset.get("Simple Name")
+ if simple_name and "Labels" in asset and "Class" in asset["Labels"]:
+ asset_class_mapping[simple_name] = asset["Labels"]["Class"]
+
+ print(f"Loaded class information for {len(asset_class_mapping)} assets")
+ except Exception as e:
+ print(f"Error loading asset info: {e}")
+
+ prop_dirs = []
+ if os.path.exists(simready_dir):
+ prop_dirs = [
+ d
+ for d in os.listdir(simready_dir)
+ if os.path.isdir(os.path.join(simready_dir, d))
+ ]
+
+ metadata = []
+
+ for prop_name in prop_dirs:
+ prop_dir = os.path.join(simready_dir, prop_name)
+
+ usd_files = glob(os.path.join(prop_dir, "*.usd"))
+ if not usd_files:
+ continue
+
+ inst_base_files = [f for f in usd_files if "_inst_base.usd" in f]
+ base_files = [f for f in usd_files if "_base.usd" in f]
+
+ if inst_base_files:
+ usd_file = inst_base_files[0]
+ elif base_files:
+ usd_file = base_files[0]
+ else:
+ usd_file = usd_files[0]
+
+ sha256 = hashlib.sha256(prop_name.encode()).hexdigest()
+
+ prop_class = asset_class_mapping.get(prop_name, default_class)
+
+ metadata.append(
+ {
+ "sha256": sha256,
+ "local_path": usd_file,
+ "original_name": prop_name,
+ "aesthetic_score": 1.0,
+ "rendered": False,
+ "class": prop_class,
+ "dataset": "simready",
+ }
+ )
+
+ return metadata
+
+
+def get_commercial_metadata(commercial_dir, default_class="commercial"):
+ metadata = []
+
+ if not os.path.exists(commercial_dir):
+ print(f"Commercial directory not found: {commercial_dir}")
+ return metadata
+
+ for root, _, files in os.walk(commercial_dir):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_file = os.path.join(root, file)
+
+ object_name = os.path.basename(os.path.dirname(usd_file))
+
+ sha256 = hashlib.sha256(f"{object_name}_{file}".encode()).hexdigest()
+
+ try:
+ material_info = common.extract_materials_from_usd(
+ usd_file, "commercial"
+ )
+ category = material_info.get("category", default_class)
+ except Exception:
+ category = default_class
+
+ metadata.append(
+ {
+ "sha256": sha256,
+ "local_path": usd_file,
+ "original_name": f"{object_name}/{file}",
+ "aesthetic_score": 1.0,
+ "rendered": False,
+ "class": category,
+ "dataset": "commercial",
+ }
+ )
+
+ return metadata
+
+
+def get_residential_metadata(residential_dir, default_class="residential"):
+ metadata = []
+
+ if not os.path.exists(residential_dir):
+ print(f"Residential directory not found: {residential_dir}")
+ return metadata
+
+ for root, _, files in os.walk(residential_dir):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_file = os.path.join(root, file)
+
+ object_name = os.path.basename(os.path.dirname(usd_file))
+
+ sha256 = hashlib.sha256(f"{object_name}_{file}".encode()).hexdigest()
+
+ try:
+ material_info = common.extract_materials_from_usd(
+ usd_file, "residential"
+ )
+ category = material_info.get("category", default_class)
+ except Exception:
+ category = default_class
+
+ metadata.append(
+ {
+ "sha256": sha256,
+ "local_path": usd_file,
+ "original_name": f"{object_name}/{file}",
+ "aesthetic_score": 1.0,
+ "rendered": False,
+ "class": category,
+ "dataset": "residential",
+ }
+ )
+
+ return metadata
+
+
+def get_vegetation_metadata(vegetation_dir, default_class="vegetation"):
+ metadata = []
+
+ if not os.path.exists(vegetation_dir):
+ print(f"Vegetation directory not found: {vegetation_dir}")
+ return metadata
+
+ for root, _, files in os.walk(vegetation_dir):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_file = os.path.join(root, file)
+
+ object_name = os.path.basename(os.path.dirname(usd_file))
+
+ sha256 = hashlib.sha256(f"{object_name}_{file}".encode()).hexdigest()
+
+ try:
+ material_info = common.extract_materials_from_usd(
+ usd_file, "vegetation"
+ )
+ category = material_info.get("category", default_class)
+ except Exception:
+ category = default_class
+
+ metadata.append(
+ {
+ "sha256": sha256,
+ "local_path": usd_file,
+ "original_name": f"{object_name}/{file}",
+ "aesthetic_score": 1.0,
+ "rendered": False,
+ "class": category,
+ "dataset": "vegetation",
+ }
+ )
+
+ return metadata
+
+
+def get_metadata(
+ simready_dir=None,
+ commercial_dir=None,
+ residential_dir=None,
+ vegetation_dir=None,
+ output_dir=None,
+ asset_info_path=None,
+ include_datasets="simready,commercial,residential,vegetation",
+ seed=42,
+ default_class="unknown",
+ skip_split=False,
+ **kwargs,
+):
+
+ set_seeds(seed)
+
+ if simready_dir is None:
+ simready_dir = SIMREADY_PROPS_DIR
+ if commercial_dir is None:
+ commercial_dir = COMMERCIAL_BASE_DIR
+ if residential_dir is None:
+ residential_dir = RESIDENTIAL_BASE_DIR
+ if vegetation_dir is None:
+ vegetation_dir = VEGETATION_BASE_DIR
+ if asset_info_path is None:
+ asset_info_path = SIMREADY_ASSET_INFO_PATH
+
+ datasets = [d.strip() for d in include_datasets.split(",")]
+
+ metadata = []
+
+ if "simready" in datasets:
+ print(f"Processing SimReady dataset from {simready_dir}")
+ simready_metadata = get_simready_metadata(
+ simready_dir, asset_info_path, default_class
+ )
+ metadata.extend(simready_metadata)
+ print(f"Added {len(simready_metadata)} items from SimReady dataset")
+
+ if "commercial" in datasets:
+ print(f"Processing Commercial dataset from {commercial_dir}")
+ commercial_metadata = get_commercial_metadata(commercial_dir)
+ metadata.extend(commercial_metadata)
+ print(f"Added {len(commercial_metadata)} items from Commercial dataset")
+
+ if "residential" in datasets:
+ print(f"Processing Residential dataset from {residential_dir}")
+ residential_metadata = get_residential_metadata(residential_dir)
+ metadata.extend(residential_metadata)
+ print(f"Added {len(residential_metadata)} items from Residential dataset")
+
+ if "vegetation" in datasets:
+ print(f"Processing Vegetation dataset from {vegetation_dir}")
+ vegetation_metadata = get_vegetation_metadata(vegetation_dir)
+ metadata.extend(vegetation_metadata)
+ print(f"Added {len(vegetation_metadata)} items from Vegetation dataset")
+
+ df = pd.DataFrame(metadata)
+
+ if df.empty:
+ print("Warning: No metadata collected from any dataset")
+ return df
+
+ class_counts = df["class"].value_counts()
+ print("\nClass distribution in combined dataset:")
+ for class_name, count in class_counts.items():
+ print(f" - {class_name}: {count} ({count/len(df)*100:.1f}%)")
+
+ dataset_counts = df["dataset"].value_counts()
+ print("\nDataset distribution:")
+ for dataset_name, count in dataset_counts.items():
+ print(f" - {dataset_name}: {count} ({count/len(df)*100:.1f}%)")
+
+ if not skip_split:
+ df = split_dataset(df, seed=seed)
+ else:
+ print("Skipping dataset splitting as requested")
+ df["split"] = "train"
+
+ if output_dir:
+ os.makedirs(output_dir, exist_ok=True)
+
+ df.to_csv(os.path.join(output_dir, "metadata.csv"), index=False)
+
+ splits_dir = os.path.join(output_dir, "splits")
+ os.makedirs(splits_dir, exist_ok=True)
+
+ for split in ["train", "val", "test"]:
+ split_df = df[df["split"] == split]
+ if not split_df.empty:
+ split_df.to_csv(os.path.join(splits_dir, f"{split}.csv"), index=False)
+
+ class_stats = df.groupby(["class", "split"]).size().unstack(fill_value=0)
+ class_stats.to_csv(os.path.join(output_dir, "class_distribution.csv"))
+
+ dataset_stats = df.groupby(["dataset", "split"]).size().unstack(fill_value=0)
+ dataset_stats.to_csv(os.path.join(output_dir, "dataset_distribution.csv"))
+
+ return df
+
+
+def foreach_instance(metadata, output_dir, func, max_workers=8, desc="Processing"):
+ from concurrent.futures import ThreadPoolExecutor
+ from tqdm import tqdm
+ import pandas as pd
+
+ results = []
+
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
+ futures = []
+ for _, row in metadata.iterrows():
+ sha256 = row["sha256"]
+ local_path = row["local_path"]
+ dataset = row.get("dataset", "unknown")
+
+ futures.append(executor.submit(func, local_path, sha256, dataset))
+
+ for future in tqdm(futures, desc=desc, total=len(futures)):
+ try:
+ result = future.result()
+ if result is not None:
+ results.append(result)
+ except Exception as e:
+ print(f"Error in worker: {e}")
+
+ return pd.DataFrame.from_records(results)
diff --git a/deps/vomp/dataset_toolkits/datasets/simready.py b/deps/vomp/dataset_toolkits/datasets/simready.py
new file mode 100644
index 0000000000000000000000000000000000000000..27e2745c46626ed567d024cd32a0fd59bdcb1c9d
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/datasets/simready.py
@@ -0,0 +1,297 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import json
+import pandas as pd
+import numpy as np
+import hashlib
+import random
+from glob import glob
+from sklearn.model_selection import train_test_split
+
+
+def set_seeds(seed=42):
+ random.seed(seed)
+ np.random.seed(seed)
+
+
+def add_args(parser):
+ parser.add_argument(
+ "--simready_dir",
+ type=str,
+ default="datasets/raw/simready/common_assets/props",
+ help="Path to the SimReady props directory",
+ )
+ parser.add_argument(
+ "--asset_info_path",
+ type=str,
+ default="datasets/raw/simready/asset_info.json",
+ help="Path to the SimReady asset_info.json file",
+ )
+ parser.add_argument(
+ "--seed",
+ type=int,
+ default=42,
+ help="Random seed for reproducibility",
+ )
+ parser.add_argument(
+ "--default_class",
+ type=str,
+ default="unknown",
+ help="Default class label to use when class information is not available",
+ )
+
+
+def get_asset_class_mapping(asset_info_path):
+ if not os.path.exists(asset_info_path):
+ print(f"Warning: Asset info file not found at {asset_info_path}")
+ return {}
+
+ try:
+ with open(asset_info_path, "r") as f:
+ asset_info = json.load(f)
+
+ asset_class_mapping = {}
+ for asset in asset_info:
+ simple_name = asset.get("Simple Name")
+ if simple_name and "Labels" in asset and "Class" in asset["Labels"]:
+ asset_class_mapping[simple_name] = asset["Labels"]["Class"]
+
+ print(f"Loaded class information for {len(asset_class_mapping)} assets")
+ return asset_class_mapping
+ except Exception as e:
+ print(f"Error loading asset info: {e}")
+ return {}
+
+
+def split_dataset(metadata, seed=42):
+
+ np.random.seed(seed)
+ random.seed(seed)
+
+ metadata_copy = metadata.copy()
+
+ metadata_copy["split"] = "train"
+
+ classes = metadata_copy["class"].unique()
+
+ large_classes = []
+ small_classes = []
+
+ for cls in classes:
+
+ cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
+ if len(cls_indices) >= 10:
+ large_classes.append(cls)
+ else:
+ small_classes.append(cls)
+
+ for cls in large_classes:
+ cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
+ random.shuffle(cls_indices)
+
+ n_samples = len(cls_indices)
+ n_train = int(0.8 * n_samples)
+ n_val = int(0.1 * n_samples)
+
+ train_indices = cls_indices[:n_train]
+ val_indices = cls_indices[n_train : n_train + n_val]
+ test_indices = cls_indices[n_train + n_val :]
+
+ metadata_copy.loc[train_indices, "split"] = "train"
+ metadata_copy.loc[val_indices, "split"] = "val"
+ metadata_copy.loc[test_indices, "split"] = "test"
+
+ total_samples = len(metadata_copy)
+ goal_train = int(0.8 * total_samples)
+ goal_val = int(0.1 * total_samples)
+ goal_test = total_samples - goal_train - goal_val
+
+ current_train = (metadata_copy["split"] == "train").sum()
+ current_val = (metadata_copy["split"] == "val").sum()
+ current_test = (metadata_copy["split"] == "test").sum()
+
+ small_indices = []
+ for cls in small_classes:
+ cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
+ small_indices.extend(cls_indices)
+
+ random.shuffle(small_indices)
+
+ need_train = max(0, goal_train - current_train)
+ need_val = max(0, goal_val - current_val)
+ need_test = max(0, goal_test - current_test)
+
+ idx = 0
+ while idx < len(small_indices):
+ if need_train > 0:
+ metadata_copy.loc[small_indices[idx], "split"] = "train"
+ need_train -= 1
+ idx += 1
+ elif need_val > 0:
+ metadata_copy.loc[small_indices[idx], "split"] = "val"
+ need_val -= 1
+ idx += 1
+ elif need_test > 0:
+ metadata_copy.loc[small_indices[idx], "split"] = "test"
+ need_test -= 1
+ idx += 1
+ else:
+
+ metadata_copy.loc[small_indices[idx:], "split"] = "train"
+ break
+
+ train_count = (metadata_copy["split"] == "train").sum()
+ val_count = (metadata_copy["split"] == "val").sum()
+ test_count = (metadata_copy["split"] == "test").sum()
+
+ print(
+ f"Dataset split: Train: {train_count} ({train_count/len(metadata_copy)*100:.1f}%), "
+ f"Val: {val_count} ({val_count/len(metadata_copy)*100:.1f}%), "
+ f"Test: {test_count} ({test_count/len(metadata_copy)*100:.1f}%)"
+ )
+
+ if small_classes:
+ print("\nSmall class distribution across splits:")
+ for cls in small_classes:
+ cls_data = metadata_copy[metadata_copy["class"] == cls]
+ cls_train = (cls_data["split"] == "train").sum()
+ cls_val = (cls_data["split"] == "val").sum()
+ cls_test = (cls_data["split"] == "test").sum()
+ cls_total = len(cls_data)
+ print(
+ f" - {cls} (total {cls_total}): Train: {cls_train}, Val: {cls_val}, Test: {cls_test}"
+ )
+
+ return metadata_copy
+
+
+def get_metadata(
+ simready_dir=None,
+ output_dir=None,
+ asset_info_path=None,
+ seed=42,
+ default_class="unknown",
+ skip_split=False,
+ **kwargs,
+):
+
+ set_seeds(seed)
+
+ if simready_dir is None:
+ simready_dir = "datasets/raw/simready/common_assets/props"
+
+ if asset_info_path is None:
+ asset_info_path = "datasets/raw/simready/asset_info.json"
+
+ asset_class_mapping = get_asset_class_mapping(asset_info_path)
+
+ prop_dirs = [
+ d
+ for d in os.listdir(simready_dir)
+ if os.path.isdir(os.path.join(simready_dir, d))
+ ]
+
+ metadata = []
+
+ for prop_name in prop_dirs:
+ prop_dir = os.path.join(simready_dir, prop_name)
+
+ usd_files = glob(os.path.join(prop_dir, "*.usd"))
+ if not usd_files:
+ continue
+
+ inst_base_files = [f for f in usd_files if "_inst_base.usd" in f]
+ base_files = [f for f in usd_files if "_base.usd" in f]
+
+ if inst_base_files:
+ usd_file = inst_base_files[0]
+ elif base_files:
+ usd_file = base_files[0]
+ else:
+ usd_file = usd_files[0]
+
+ sha256 = hashlib.sha256(prop_name.encode()).hexdigest()
+
+ prop_class = asset_class_mapping.get(prop_name, default_class)
+
+ metadata.append(
+ {
+ "sha256": sha256,
+ "local_path": usd_file,
+ "original_name": prop_name,
+ "aesthetic_score": 1.0,
+ "rendered": False,
+ "class": prop_class,
+ }
+ )
+
+ df = pd.DataFrame(metadata)
+
+ class_counts = df["class"].value_counts()
+ print("\nClass distribution in dataset:")
+ for class_name, count in class_counts.items():
+ print(f" - {class_name}: {count} ({count/len(df)*100:.1f}%)")
+
+ if not skip_split:
+ df = split_dataset(df, seed=seed)
+ else:
+ print("Skipping dataset splitting as requested")
+ df["split"] = "train"
+
+ if output_dir:
+ os.makedirs(output_dir, exist_ok=True)
+
+ df.to_csv(os.path.join(output_dir, "metadata.csv"), index=False)
+
+ splits_dir = os.path.join(output_dir, "splits")
+ os.makedirs(splits_dir, exist_ok=True)
+
+ for split in ["train", "val", "test"]:
+ split_df = df[df["split"] == split]
+ if not split_df.empty:
+ split_df.to_csv(os.path.join(splits_dir, f"{split}.csv"), index=False)
+
+ class_stats = df.groupby(["class", "split"]).size().unstack(fill_value=0)
+ class_stats.to_csv(os.path.join(output_dir, "class_distribution.csv"))
+
+ return df
+
+
+def foreach_instance(metadata, output_dir, func, max_workers=8, desc="Processing"):
+ from concurrent.futures import ThreadPoolExecutor
+ from tqdm import tqdm
+ import pandas as pd
+
+ results = []
+
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
+ futures = []
+ for _, row in metadata.iterrows():
+ sha256 = row["sha256"]
+ local_path = row["local_path"]
+ futures.append(executor.submit(func, local_path, sha256))
+
+ for future in tqdm(futures, desc=desc, total=len(futures)):
+ try:
+ result = future.result()
+ if result is not None:
+ results.append(result)
+ except Exception as e:
+ print(f"Error in worker: {e}")
+
+ return pd.DataFrame.from_records(results)
diff --git a/deps/vomp/dataset_toolkits/extract_feature.py b/deps/vomp/dataset_toolkits/extract_feature.py
new file mode 100644
index 0000000000000000000000000000000000000000..32521998411455e25671486f70d11013587c5217
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/extract_feature.py
@@ -0,0 +1,273 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import copy
+import sys
+import json
+import importlib
+import argparse
+import torch
+import torch.nn.functional as F
+import numpy as np
+import pandas as pd
+import utils3d
+from tqdm import tqdm
+from easydict import EasyDict as edict
+from torchvision import transforms
+from PIL import Image
+
+torch.set_grad_enabled(False)
+
+
+def get_data(frames, sha256):
+ valid_data = []
+
+ for view in frames:
+ image_path = os.path.join(opt.output_dir, "renders", sha256, view["file_path"])
+ try:
+ # Check if file exists before trying to open it
+ if not os.path.exists(image_path):
+ print(f"Warning: Image file {image_path} not found, skipping")
+ continue
+
+ image = Image.open(image_path)
+ except Exception as e:
+ print(f"Error loading image {image_path}: {e}")
+ continue
+
+ try:
+ image = image.resize((518, 518), Image.Resampling.LANCZOS)
+ image = np.array(image).astype(np.float32) / 255
+ image = image[:, :, :3] * image[:, :, 3:]
+ image = torch.from_numpy(image).permute(2, 0, 1).float()
+
+ c2w = torch.tensor(view["transform_matrix"])
+ c2w[:3, 1:3] *= -1
+ extrinsics = torch.inverse(c2w)
+ fov = view["camera_angle_x"]
+ intrinsics = utils3d.torch.intrinsics_from_fov_xy(
+ torch.tensor(fov), torch.tensor(fov)
+ )
+
+ valid_data.append(
+ {"image": image, "extrinsics": extrinsics, "intrinsics": intrinsics}
+ )
+ except Exception as e:
+ print(f"Error processing image {image_path}: {e}")
+ continue
+
+ if len(valid_data) == 0:
+ print(f"Warning: No valid images found for {sha256}")
+ else:
+ print(f"Loaded {len(valid_data)}/{len(frames)} valid images for {sha256}")
+
+ return valid_data
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--output_dir", type=str, required=True, help="Directory to save the metadata"
+ )
+ parser.add_argument(
+ "--filter_low_aesthetic_score",
+ type=float,
+ default=None,
+ help="Filter objects with aesthetic score lower than this value",
+ )
+ parser.add_argument(
+ "--model",
+ type=str,
+ default="dinov2_vitl14_reg",
+ help="Feature extraction model",
+ )
+ parser.add_argument(
+ "--instances", type=str, default=None, help="Instances to process"
+ )
+ parser.add_argument("--batch_size", type=int, default=16)
+ parser.add_argument("--rank", type=int, default=0)
+ parser.add_argument("--world_size", type=int, default=1)
+ parser.add_argument(
+ "--force",
+ action="store_true",
+ help="Force feature extraction even if feature files already exist",
+ )
+ opt = parser.parse_args()
+ opt = edict(vars(opt))
+
+ feature_name = opt.model
+ os.makedirs(os.path.join(opt.output_dir, "features", feature_name), exist_ok=True)
+
+ # load model
+ dinov2_model = torch.hub.load("facebookresearch/dinov2", opt.model)
+ dinov2_model.eval().cuda()
+ transform = transforms.Compose(
+ [
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
+ ]
+ )
+ n_patch = 518 // 14
+
+ # get file list
+ if os.path.exists(os.path.join(opt.output_dir, "metadata.csv")):
+ metadata = pd.read_csv(os.path.join(opt.output_dir, "metadata.csv"))
+ else:
+ raise ValueError("metadata.csv not found")
+ if opt.instances is not None:
+ with open(opt.instances, "r") as f:
+ instances = f.read().splitlines()
+ metadata = metadata[metadata["sha256"].isin(instances)]
+ else:
+ if opt.filter_low_aesthetic_score is not None:
+ metadata = metadata[
+ metadata["aesthetic_score"] >= opt.filter_low_aesthetic_score
+ ]
+ if f"feature_{feature_name}" in metadata.columns and not opt.force:
+ metadata = metadata[metadata[f"feature_{feature_name}"] == False]
+ metadata = metadata[metadata["voxelized"] == True]
+ metadata = metadata[metadata["rendered"] == True]
+
+ start = len(metadata) * opt.rank // opt.world_size
+ end = len(metadata) * (opt.rank + 1) // opt.world_size
+ metadata = metadata[start:end]
+ records = []
+
+ # filter out objects that are already processed
+ sha256s = list(metadata["sha256"].values)
+ if not opt.force:
+ for sha256 in copy.copy(sha256s):
+ if os.path.exists(
+ os.path.join(opt.output_dir, "features", feature_name, f"{sha256}.npz")
+ ):
+ records.append({"sha256": sha256, f"feature_{feature_name}": True})
+ sha256s.remove(sha256)
+ else:
+ print(
+ f"Force mode enabled. Processing all {len(sha256s)} objects regardless of existing features."
+ )
+
+ # filter out objects that don't have voxel files
+ initial_count = len(sha256s)
+ sha256s_with_voxels = []
+ for sha256 in sha256s:
+ voxel_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ if os.path.exists(voxel_path):
+ sha256s_with_voxels.append(sha256)
+ else:
+ print(f"Skipping {sha256}: voxel file not found at {voxel_path}")
+
+ sha256s = sha256s_with_voxels
+ print(f"Filtered from {initial_count} to {len(sha256s)} objects with voxel files")
+
+ # extract features
+ for sha256 in tqdm(sha256s, desc="Extracting features"):
+ try:
+ # Load data
+ with open(
+ os.path.join(opt.output_dir, "renders", sha256, "transforms.json"),
+ "r",
+ ) as f:
+ metadata_json = json.load(f)
+ frames = metadata_json["frames"]
+ data = get_data(frames, sha256)
+
+ if len(data) == 0:
+ print(f"Skipping {sha256}: no valid image data")
+ continue
+
+ # Apply transform to images
+ for datum in data:
+ datum["image"] = transform(datum["image"])
+
+ # Load positions
+ positions = utils3d.io.read_ply(
+ os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
+ )[0]
+ positions = torch.from_numpy(positions).float().cuda()
+ indices = ((positions + 0.5) * 64).long()
+ # Clamp indices to valid range [0, 63] to handle floating point precision issues
+ indices = torch.clamp(indices, 0, 63)
+
+ n_views = len(data)
+ N = positions.shape[0]
+ pack = {
+ "indices": indices.cpu().numpy().astype(np.uint8),
+ }
+
+ patchtokens_lst = []
+ uv_lst = []
+
+ # Process in batches
+ for i in range(0, n_views, opt.batch_size):
+ batch_data = data[i : i + opt.batch_size]
+ bs = len(batch_data)
+ batch_images = torch.stack([d["image"] for d in batch_data]).cuda()
+ batch_extrinsics = torch.stack(
+ [d["extrinsics"] for d in batch_data]
+ ).cuda()
+ batch_intrinsics = torch.stack(
+ [d["intrinsics"] for d in batch_data]
+ ).cuda()
+ features = dinov2_model(batch_images, is_training=True)
+ uv = (
+ utils3d.torch.project_cv(
+ positions, batch_extrinsics, batch_intrinsics
+ )[0]
+ * 2
+ - 1
+ )
+ patchtokens = (
+ features["x_prenorm"][:, dinov2_model.num_register_tokens + 1 :]
+ .permute(0, 2, 1)
+ .reshape(bs, 1024, n_patch, n_patch)
+ )
+ patchtokens_lst.append(patchtokens)
+ uv_lst.append(uv)
+
+ patchtokens = torch.cat(patchtokens_lst, dim=0)
+ uv = torch.cat(uv_lst, dim=0)
+
+ # Save features
+ pack["patchtokens"] = (
+ F.grid_sample(
+ patchtokens,
+ uv.unsqueeze(1),
+ mode="bilinear",
+ align_corners=False,
+ )
+ .squeeze(2)
+ .permute(0, 2, 1)
+ .cpu()
+ .numpy()
+ )
+ pack["patchtokens"] = np.mean(pack["patchtokens"], axis=0).astype(
+ np.float16
+ )
+ save_path = os.path.join(
+ opt.output_dir, "features", feature_name, f"{sha256}.npz"
+ )
+ np.savez_compressed(save_path, **pack)
+ records.append({"sha256": sha256, f"feature_{feature_name}": True})
+
+ except Exception as e:
+ print(f"Error processing {sha256}: {e}")
+ continue
+
+ records = pd.DataFrame.from_records(records)
+ records.to_csv(
+ os.path.join(opt.output_dir, f"feature_{feature_name}_{opt.rank}.csv"),
+ index=False,
+ )
diff --git a/deps/vomp/dataset_toolkits/latent_space/analyze_data_distribution.py b/deps/vomp/dataset_toolkits/latent_space/analyze_data_distribution.py
new file mode 100644
index 0000000000000000000000000000000000000000..0938d36e4ecad34d7067767c5becfabff32715dc
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/latent_space/analyze_data_distribution.py
@@ -0,0 +1,111 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from pathlib import Path
+
+df = pd.read_csv("datasets/latent_space/materials.csv")
+
+print("Data shape:", df.shape)
+print("\nColumn names:", df.columns.tolist())
+
+
+for col in ["youngs_modulus", "poisson_ratio", "density"]:
+ print(f"\n{col}:")
+ print(f" Min: {df[col].min():.2e}")
+ print(f" Max: {df[col].max():.2e}")
+ print(f" Mean: {df[col].mean():.2e}")
+ print(f" Median: {df[col].median():.2e}")
+ print(f" Std: {df[col].std():.2e}")
+
+ Q1 = df[col].quantile(0.25)
+ Q3 = df[col].quantile(0.75)
+ IQR = Q3 - Q1
+ lower_bound = Q1 - 1.5 * IQR
+ upper_bound = Q3 + 1.5 * IQR
+
+ outliers = df[(df[col] < lower_bound) | (df[col] > upper_bound)]
+ print(
+ f" Outliers (IQR method): {len(outliers)} ({len(outliers)/len(df)*100:.1f}%)"
+ )
+
+ if col in ["youngs_modulus", "density"]:
+ log_vals = np.log10(df[col])
+ print(f" Log10 range: [{log_vals.min():.2f}, {log_vals.max():.2f}]")
+ print(
+ f" Log10 span: {log_vals.max() - log_vals.min():.2f} orders of magnitude"
+ )
+
+
+print("\n\nMaterials with extreme Young's modulus (< 1e7 Pa):")
+low_E = df[df["youngs_modulus"] < 1e7]
+if len(low_E) > 0:
+ material_counts = low_E["material_name"].value_counts().head(10)
+ for mat, count in material_counts.items():
+ print(f" {mat}: {count} samples")
+
+print("\n\nMaterials with extreme density (< 100 kg/mยณ):")
+low_rho = df[df["density"] < 100]
+if len(low_rho) > 0:
+ material_counts = low_rho["material_name"].value_counts().head(10)
+ for mat, count in material_counts.items():
+ print(f" {mat}: {count} samples")
+
+
+print("\n\nPercentile analysis:")
+percentiles = [1, 5, 10, 25, 50, 75, 90, 95, 99]
+for col in ["youngs_modulus", "poisson_ratio", "density"]:
+ print(f"\n{col} percentiles:")
+ for p in percentiles:
+ val = df[col].quantile(p / 100)
+ print(f" {p}%: {val:.2e}")
+
+
+print("\n\nCreating filtered dataset...")
+
+filtered_df = df[
+ (df["youngs_modulus"] >= 1e5)
+ & (df["youngs_modulus"] <= 1e12)
+ & (df["density"] >= 100)
+ & (df["density"] <= 20000)
+ & (df["poisson_ratio"] >= 0.0)
+ & (df["poisson_ratio"] <= 0.49)
+]
+
+print(f"Original size: {len(df)}")
+print(f"Filtered size: {len(filtered_df)}")
+print(
+ f"Removed: {len(df) - len(filtered_df)} ({(len(df) - len(filtered_df))/len(df)*100:.1f}%)"
+)
+
+
+print("\nRanges in filtered dataset (only Poisson ratio filtering):")
+for col in ["youngs_modulus", "poisson_ratio", "density"]:
+ print(f"\n{col}:")
+ print(f" Min: {filtered_df[col].min():.2e}")
+ print(f" Max: {filtered_df[col].max():.2e}")
+ print(f" Range span: {filtered_df[col].max() - filtered_df[col].min():.2e}")
+
+ if col in ["youngs_modulus", "density"]:
+ log_min = np.log10(filtered_df[col].min())
+ log_max = np.log10(filtered_df[col].max())
+ print(f" Log10 range: [{log_min:.2f}, {log_max:.2f}]")
+ print(f" Orders of magnitude: {log_max - log_min:.2f}")
+
+filtered_df.to_csv("datasets/latent_space/materials_filtered.csv", index=False)
+print("\nSaved filtered dataset to materials_filtered.csv")
diff --git a/deps/vomp/dataset_toolkits/latent_space/make_csv.py b/deps/vomp/dataset_toolkits/latent_space/make_csv.py
new file mode 100644
index 0000000000000000000000000000000000000000..62c438132071b1ced400256e964f60d8410feb6c
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/latent_space/make_csv.py
@@ -0,0 +1,411 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import argparse
+import json
+import csv
+import random
+from pathlib import Path
+from typing import Tuple, Set, List
+import math
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ parse_numerical_range_str,
+)
+
+
+def parse_args() -> Path:
+ parser = argparse.ArgumentParser(
+ description="Generate a materials.csv file from material_ranges.csv in the provided directory."
+ )
+ parser.add_argument(
+ "directory",
+ type=str,
+ help="Path to the directory containing material_ranges.csv",
+ )
+ args = parser.parse_args()
+ directory = Path(args.directory).expanduser().resolve()
+
+ if not directory.is_dir():
+ parser.error(f"Provided path '{directory}' is not a directory.")
+
+ return directory
+
+
+def read_dataset(json_path: Path):
+ try:
+ with json_path.open("r", encoding="utf-8") as f:
+ return json.load(f)
+ except FileNotFoundError:
+ raise FileNotFoundError(
+ f"Could not find '{json_path}'. Ensure the directory contains the file."
+ )
+
+
+def extract_unique_rows(
+ dataset, unique_triplets: Set[Tuple[float, float, float]] | None = None
+) -> tuple[list, Set[Tuple[float, float, float]]]:
+ if unique_triplets is None:
+ unique_triplets = set()
+
+ rows: List[Tuple[str, float, float, float]] = []
+
+ for obj in dataset:
+ segments = obj.get("segments", {})
+ for seg_key, seg_data in segments.items():
+ try:
+ youngs = float(seg_data["youngs_modulus"])
+ poisson = float(seg_data["poissons_ratio"])
+ density = float(seg_data["density"])
+ except (KeyError, ValueError, TypeError):
+ continue
+
+ if youngs <= 0 or youngs > 1e13:
+ print(
+ f"WARNING: Skipping material with invalid Young's modulus: {youngs}"
+ )
+ continue
+
+ if poisson < -1.0 or poisson > 0.5:
+ print(
+ f"WARNING: Skipping material with invalid Poisson's ratio: {poisson}"
+ )
+ continue
+
+ if density <= 0 or density > 50000:
+ print(f"WARNING: Skipping material with invalid density: {density}")
+ continue
+
+ triplet = (youngs, poisson, density)
+ if triplet in unique_triplets:
+ continue
+
+ unique_triplets.add(triplet)
+ material_name = seg_data.get("name", seg_key)
+ rows.append((material_name, youngs, poisson, density))
+
+ return rows, unique_triplets
+
+
+def sample_ranges(
+ csv_path: Path,
+ unique_triplets: Set[Tuple[float, float, float]],
+ min_samples_per_material: int = 100,
+ max_samples_per_material: int = 2500,
+ target_total_samples: int = 100_000,
+) -> list:
+ if not csv_path.exists():
+
+ return []
+
+ parsed_rows: list[dict] = []
+ dynamic_indices: list[int] = []
+
+ with csv_path.open("r", encoding="utf-8") as f:
+ lines = f.readlines()
+
+ header = lines[0].strip().split(",")
+
+ for idx, line in enumerate(lines[1:], 0):
+
+ parts = []
+ current = ""
+ in_brackets = False
+ for char in line.strip() + ",":
+ if char == "," and not in_brackets:
+ parts.append(current)
+ current = ""
+ else:
+ if char == "[":
+ in_brackets = True
+ elif char == "]":
+ in_brackets = False
+ current += char
+
+ if len(parts) < 4:
+ print(f"WARNING: Line {idx+1} has incorrect format: {line.strip()}")
+ continue
+
+ material_name = parts[0].strip().strip('"')
+
+ y_range_str = parts[1].strip().strip('"')
+ p_range_str = parts[2].strip().strip('"')
+ d_range_str = parts[3].strip().strip('"')
+
+ try:
+ y_low, y_high = parse_numerical_range_str(y_range_str)
+ p_low, p_high = parse_numerical_range_str(p_range_str)
+ d_low, d_high = parse_numerical_range_str(d_range_str)
+ except ValueError as e:
+ print(
+ f"WARNING: Error parsing ranges for {material_name} on line {idx+1}: {e} - Skipping material."
+ )
+ continue
+
+ y_low *= 1e9
+ y_high *= 1e9
+
+ y_low = max(1e6, min(y_low, 1e13))
+ y_high = max(y_low, min(y_high, 1e13))
+
+ p_low = max(-0.999, min(p_low, 0.499))
+ p_high = max(p_low, min(p_high, 0.499))
+
+ d_low = max(10.0, min(d_low, 50000.0))
+ d_high = max(d_low, min(d_high, 50000.0))
+
+ y_has_range = abs(y_high - y_low) > 1e-6
+ p_has_range = abs(p_high - p_low) > 1e-6
+ d_has_range = abs(d_high - d_low) > 1e-6
+
+ has_range = y_has_range or p_has_range or d_has_range
+
+ y_width = max(y_high - y_low, 1.0) if y_has_range else 1.0
+ p_width = max(p_high - p_low, 0.001) if p_has_range else 0.001
+ d_width = max(d_high - d_low, 1.0) if d_has_range else 1.0
+
+ y_width_norm = y_width / 1e9
+
+ volume = y_width_norm * p_width * d_width
+
+ if has_range:
+ dynamic_indices.append(idx)
+
+ parsed_rows.append(
+ {
+ "material_name": material_name,
+ "y_low": y_low,
+ "y_high": y_high,
+ "p_low": p_low,
+ "p_high": p_high,
+ "d_low": d_low,
+ "d_high": d_high,
+ "has_range": has_range,
+ "y_has_range": y_has_range,
+ "p_has_range": p_has_range,
+ "d_has_range": d_has_range,
+ "volume": volume,
+ }
+ )
+
+ if not parsed_rows:
+ return []
+
+ print(
+ f"Found {len(dynamic_indices)} materials with ranges out of {len(parsed_rows)} total"
+ )
+
+ fixed_count = len(parsed_rows) - len(dynamic_indices)
+ print(f"Number of materials with fixed values: {fixed_count}")
+
+ if dynamic_indices:
+ print("\nExample materials with ranges:")
+ for i in range(min(5, len(dynamic_indices))):
+ idx = dynamic_indices[i]
+ info = parsed_rows[idx]
+ ranges_info = []
+ if info["y_has_range"]:
+ ranges_info.append(
+ f"Young's: {info['y_low']/1e9:.3f}-{info['y_high']/1e9:.3f} GPa"
+ )
+ if info["p_has_range"]:
+ ranges_info.append(
+ f"Poisson's: {info['p_low']:.3f}-{info['p_high']:.3f}"
+ )
+ if info["d_has_range"]:
+ ranges_info.append(
+ f"Density: {info['d_low']:.1f}-{info['d_high']:.1f} kg/mยณ"
+ )
+
+ print(
+ f" {info['material_name']}: {', '.join(ranges_info)} (volume: {info['volume']:.4f})"
+ )
+
+ total_volume = sum(parsed_rows[idx]["volume"] for idx in dynamic_indices)
+ print(f"\nTotal parameter space volume: {total_volume:.4f}")
+
+ volume_scale_factor = 13.0
+
+ samples_per_material = {}
+
+ for idx in dynamic_indices:
+ volume_ratio = parsed_rows[idx]["volume"] / total_volume
+ proportional_samples = max(
+ math.ceil(target_total_samples * volume_ratio * volume_scale_factor),
+ min_samples_per_material,
+ )
+
+ samples_per_material[idx] = min(proportional_samples, max_samples_per_material)
+
+ fixed_total = 0
+
+ dynamic_total = sum(samples_per_material.values())
+ total_planned = dynamic_total + fixed_total
+
+ print(f"\nSampling strategy (scaled by {volume_scale_factor}x):")
+ print(f" Minimum samples per material with ranges: {min_samples_per_material}")
+ print(f" Maximum samples per material: {max_samples_per_material}")
+ print(f" Planned total samples: {total_planned}")
+
+ sorted_materials = sorted(
+ [
+ (
+ idx,
+ parsed_rows[idx]["material_name"],
+ parsed_rows[idx]["volume"],
+ samples_per_material.get(idx, 1) if idx in dynamic_indices else 1,
+ )
+ for idx in range(len(parsed_rows))
+ ],
+ key=lambda x: x[2],
+ reverse=True,
+ )
+
+ print("\nTop 15 highest volume materials:")
+ for idx, name, volume, samples in sorted_materials[:15]:
+ if idx in dynamic_indices:
+ volume_percent = volume / total_volume * 100
+ print(
+ f" {name}: volume {volume:.4f} ({volume_percent:.2f}%), {samples} samples"
+ )
+ else:
+ print(f" {name}: fixed values, 1 sample")
+
+ rows: list[Tuple[str, float, float, float]] = []
+
+ def _add_triplet(material: str, y: float, p: float, d: float):
+
+ if y <= 0 or y > 1e13:
+ return False
+ if p < -1.0 or p > 0.5:
+ return False
+ if d <= 0 or d > 50000:
+ return False
+
+ triplet = (y, p, d)
+ if triplet in unique_triplets:
+ return False
+ unique_triplets.add(triplet)
+ rows.append((material, y, p, d))
+ return True
+
+ total_generated = 0
+ duplicate_avoidance_failures = 0
+
+ for idx, info in enumerate(parsed_rows):
+ if not info["has_range"]:
+ name = info["material_name"]
+ y_val = info["y_low"]
+ p_val = info["p_low"]
+ d_val = info["d_low"]
+
+ if _add_triplet(name, y_val, p_val, d_val):
+ total_generated += 1
+
+ print(f"Added {total_generated} materials with fixed values")
+
+ for idx in dynamic_indices:
+ info = parsed_rows[idx]
+ name = info["material_name"]
+ y_low, y_high = info["y_low"], info["y_high"]
+ p_low, p_high = info["p_low"], info["p_high"]
+ d_low, d_high = info["d_low"], info["d_high"]
+
+ required = samples_per_material.get(idx, 0)
+
+ report_progress = required > 100
+
+ attempts = 0
+ generated = 0
+
+ max_attempts = required * 50
+
+ if info["volume"] > 10.0:
+ max_attempts *= 2
+
+ if report_progress:
+ print(
+ f"Generating {required} samples for {name} (volume: {info['volume']:.4f})"
+ )
+
+ while generated < required and attempts < max_attempts:
+ attempts += 1
+
+ y_val = random.uniform(y_low, y_high) if info["y_has_range"] else y_low
+ p_val = random.uniform(p_low, p_high) if info["p_has_range"] else p_low
+ d_val = random.uniform(d_low, d_high) if info["d_has_range"] else d_low
+
+ y_val = round(y_val, 10)
+ p_val = round(p_val, 10)
+ d_val = round(d_val, 10)
+
+ if _add_triplet(name, y_val, p_val, d_val):
+ generated += 1
+ total_generated += 1
+ else:
+
+ duplicate_avoidance_failures += 1
+
+ if report_progress and generated > 0 and generated % 100 == 0:
+ print(f" Generated {generated}/{required} samples for {name}")
+
+ if required > 0 and report_progress:
+
+ success_rate = (generated / attempts) * 100 if attempts > 0 else 0
+ print(
+ f"Material {name}: Generated {generated}/{required} samples after {attempts} attempts (success rate: {success_rate:.1f}%)"
+ )
+
+ print(f"Successfully generated {len(rows)} unique material property combinations")
+ print(
+ f"Duplicate avoidance prevented {duplicate_avoidance_failures} potential duplicates"
+ )
+ return rows
+
+
+def write_csv(rows: list, csv_path: Path):
+ csv_path.parent.mkdir(parents=True, exist_ok=True)
+ with csv_path.open("w", newline="", encoding="utf-8") as csvfile:
+ writer = csv.writer(csvfile)
+ writer.writerow(["material_name", "youngs_modulus", "poisson_ratio", "density"])
+ for row in rows:
+ writer.writerow(row)
+
+
+def main():
+ directory = parse_args()
+ csv_path = directory / "materials.csv"
+
+ print("Generating materials data from ranges only (skipping JSON file)...")
+
+ unique_triplets = set()
+
+ ranges_csv_path = directory / "material_ranges.csv"
+ if not ranges_csv_path.exists():
+ print(f"ERROR: material_ranges.csv not found at {ranges_csv_path}")
+ return
+
+ sampled_rows = sample_ranges(ranges_csv_path, unique_triplets)
+
+ write_csv(sampled_rows, csv_path)
+
+ print(
+ f"materials.csv generated with {len(sampled_rows)} unique rows at '{csv_path}'."
+ )
+ print("All data generated from material_ranges.csv with validation applied.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/dataset_toolkits/material_objects/render_usd.py b/deps/vomp/dataset_toolkits/material_objects/render_usd.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d4dbd57caf5f4c1700e3a9dc262b6e8e4611131
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/render_usd.py
@@ -0,0 +1,1176 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Clean USD rendering pipeline.
+
+This script extracts meshes and textures directly from USD files,
+similar to how Omniverse exports meshes. It does NOT search the filesystem
+for textures - all texture paths come from the USD shaders themselves.
+
+For vegetation datasets that use MDL materials, it parses the MDL files
+to extract texture references.
+"""
+
+import os
+import sys
+import json
+import re
+import argparse
+import tempfile
+import shutil
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+from collections import defaultdict
+from subprocess import call, DEVNULL
+
+import numpy as np
+import pandas as pd
+from pxr import Usd, UsdGeom, UsdShade, Sdf, Gf
+
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from utils import sphere_hammersley_sequence
+
+BLENDER_LINK = (
+ "https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
+)
+BLENDER_INSTALLATION_PATH = "/tmp"
+BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
+
+
+class USDMaterialExtractor:
+ """
+ Extracts materials and textures directly from USD files.
+
+ This class reads shader inputs from USD prims and resolves texture paths
+ relative to the USD file. For MDL materials (used in vegetation), it
+ parses the MDL files to extract texture references.
+ """
+
+ # MDL texture patterns (for vegetation)
+ MDL_TEXTURE_PATTERNS = [
+ r'diffuse_texture:\s*texture_2d\("([^"]+)"',
+ r'normalmap_texture:\s*texture_2d\("([^"]+)"',
+ r'reflectionroughness_texture:\s*texture_2d\("([^"]+)"',
+ r'metallic_texture:\s*texture_2d\("([^"]+)"',
+ r'ORM_texture:\s*texture_2d\("([^"]+)"',
+ ]
+
+ def __init__(self, usd_path: str, verbose: bool = False):
+ self.usd_path = Path(usd_path).resolve()
+ self.usd_dir = self.usd_path.parent
+ self.verbose = verbose
+ self.stage = None
+
+ # Extracted data
+ self.materials = {} # material_path -> {input_name: texture_path}
+ self.meshes = {} # mesh_path -> {name, material, vertices, faces, uvs}
+ self.mesh_materials = {} # mesh_path -> material_path
+
+ def _log(self, msg: str):
+ if self.verbose:
+ print(msg)
+
+ def _resolve_texture_path(self, texture_path: str) -> Optional[Path]:
+ texture_path = texture_path.strip("@")
+
+ # Handle UDIM textures - replace with first available tile
+ if "" in texture_path:
+ # Try common UDIM tile numbers
+ for udim in ["1001", "1002", "1003", "1004"]:
+ resolved = self._resolve_texture_path(
+ texture_path.replace("", udim)
+ )
+ if resolved:
+ return resolved
+ return None
+
+ # Already absolute
+ if Path(texture_path).is_absolute():
+ p = Path(texture_path)
+ return p if p.exists() else None
+
+ # Try relative to USD directory
+ candidates = [
+ self.usd_dir / texture_path,
+ self.usd_dir / "textures" / Path(texture_path).name,
+ self.usd_dir / "Textures" / Path(texture_path).name,
+ self.usd_dir / ".." / texture_path,
+ self.usd_dir / ".." / "textures" / Path(texture_path).name,
+ self.usd_dir / ".." / "materials" / "textures" / Path(texture_path).name,
+ ]
+
+ for p in candidates:
+ if p.exists():
+ return p.resolve()
+
+ # Fuzzy matching: look for files containing the texture name
+ texture_name = Path(texture_path).stem # e.g., "Iron_BaseColor"
+ texture_ext = Path(texture_path).suffix # e.g., ".png"
+
+ # Search in Textures folders
+ search_dirs = [
+ self.usd_dir / "Textures",
+ self.usd_dir / "textures",
+ self.usd_dir / ".." / "Textures",
+ self.usd_dir / ".." / "textures",
+ ]
+
+ for search_dir in search_dirs:
+ if search_dir.exists():
+ for f in search_dir.iterdir():
+ # Check if the file contains the texture name (fuzzy match)
+ if (
+ texture_name in f.stem
+ and f.suffix.lower() == texture_ext.lower()
+ ):
+ self._log(f" (fuzzy match: {texture_name} -> {f.name})")
+ return f.resolve()
+
+ return None
+
+ def _categorize_input(self, name: str) -> str:
+ name_lower = name.lower()
+
+ # Check for texture/color type based on common patterns
+ if any(
+ x in name_lower for x in ["diffuse", "albedo", "basecolor", "base_color"]
+ ):
+ return "diffuse"
+ elif any(x in name_lower for x in ["normal", "bump"]):
+ return "normal"
+ elif any(x in name_lower for x in ["rough"]):
+ return "roughness"
+ elif any(x in name_lower for x in ["metal"]):
+ return "metallic"
+ elif any(x in name_lower for x in ["orm", "occlusion"]):
+ return "orm"
+ elif any(x in name_lower for x in ["opacity", "alpha"]):
+ return "opacity"
+ else:
+ return name # Use original name if no match
+
+ def _find_fallback_textures(self, material_name: str) -> Dict[str, str]:
+ textures = {}
+
+ # Search in Textures folders
+ search_dirs = [
+ self.usd_dir / "Textures",
+ self.usd_dir / "textures",
+ ]
+
+ for search_dir in search_dirs:
+ if not search_dir.exists():
+ continue
+
+ # Find all unique texture prefixes (e.g., BlueRug from BlueRug_BaseColor.png)
+ texture_files = list(search_dir.glob("*.png")) + list(
+ search_dir.glob("*.jpg")
+ )
+ if not texture_files:
+ continue
+
+ # Group by prefix (before _BaseColor, _N, _R, etc.)
+ prefixes = set()
+ for f in texture_files:
+ stem = f.stem
+ for suffix in [
+ "_BaseColor",
+ "_basecolor",
+ "_A",
+ "_albedo",
+ "_diffuse",
+ "_N",
+ "_Normal",
+ "_normal",
+ "_R",
+ "_Roughness",
+ "_roughness",
+ ]:
+ if suffix in stem:
+ prefix = stem.split(suffix)[0]
+ prefixes.add(prefix)
+ break
+
+ # Use the first available texture set
+ if prefixes:
+ prefix = sorted(prefixes)[0] # Pick first alphabetically
+ self._log(f" (fallback: using {prefix}_* textures)")
+
+ # Find matching textures
+ for f in texture_files:
+ if f.stem.startswith(prefix):
+ stem_lower = f.stem.lower()
+ if any(
+ x in stem_lower
+ for x in ["basecolor", "_a", "albedo", "diffuse"]
+ ):
+ textures["diffuse"] = str(f.resolve())
+ self._log(f" โ fallback diffuse: {f.name}")
+ elif any(x in stem_lower for x in ["normal", "_n"]):
+ textures["normal"] = str(f.resolve())
+ self._log(f" โ fallback normal: {f.name}")
+ elif any(x in stem_lower for x in ["rough", "_r"]):
+ textures["roughness"] = str(f.resolve())
+ self._log(f" โ fallback roughness: {f.name}")
+
+ if textures:
+ return textures
+
+ return textures
+
+ def _extract_textures_from_shader(self, shader: UsdShade.Shader) -> Dict[str, any]:
+ result = {}
+
+ for shader_input in shader.GetInputs():
+ val = shader_input.Get()
+ if val is None:
+ continue
+
+ input_name = shader_input.GetBaseName()
+ category = self._categorize_input(input_name)
+
+ # Texture path (AssetPath)
+ if isinstance(val, Sdf.AssetPath) and val.path:
+ texture_path = val.path.strip("@")
+ resolved = self._resolve_texture_path(texture_path)
+ if resolved:
+ result[category] = str(resolved)
+ self._log(f" โ {input_name} -> {category}: {resolved.name}")
+ else:
+ self._log(f" โ {input_name}: {texture_path} (not resolved)")
+
+ # Color value (Vec3)
+ elif (
+ hasattr(val, "__len__")
+ and len(val) == 3
+ and "color" in input_name.lower()
+ ):
+ result[f"{category}_color"] = (
+ float(val[0]),
+ float(val[1]),
+ float(val[2]),
+ )
+ self._log(
+ f" โ {input_name} -> {category}_color: ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f})"
+ )
+
+ return result
+
+ def _extract_textures_from_mdl(self, mdl_path: Path) -> Dict[str, str]:
+ textures = {}
+
+ if not mdl_path.exists():
+ return textures
+
+ try:
+ content = mdl_path.read_text()
+
+ # Parse texture references
+ type_mapping = {
+ "diffuse_texture": "diffuse",
+ "normalmap_texture": "normal",
+ "reflectionroughness_texture": "roughness",
+ "metallic_texture": "metallic",
+ "ORM_texture": "orm",
+ }
+
+ for tex_type, canonical_name in type_mapping.items():
+ pattern = rf'{tex_type}:\s*texture_2d\("([^"]+)"'
+ match = re.search(pattern, content)
+ if match:
+ rel_path = match.group(1)
+ # MDL paths are relative to the MDL file location
+ resolved = self._resolve_texture_path(rel_path)
+ if not resolved:
+ # Try relative to MDL file directory
+ mdl_dir = mdl_path.parent
+ candidates = [
+ mdl_dir / rel_path,
+ mdl_dir / "textures" / Path(rel_path).name,
+ ]
+ for c in candidates:
+ if c.exists():
+ resolved = c.resolve()
+ break
+
+ if resolved:
+ textures[canonical_name] = str(resolved)
+ self._log(f" โ {canonical_name}: {resolved.name} (from MDL)")
+ else:
+ self._log(
+ f" โ {canonical_name}: {rel_path} (MDL, not resolved)"
+ )
+
+ except Exception as e:
+ self._log(f" Error parsing MDL {mdl_path}: {e}")
+
+ return textures
+
+ def _find_mdl_for_material(self, material_prim: Usd.Prim) -> Optional[Path]:
+ for child in material_prim.GetChildren():
+ if child.GetTypeName() == "Shader":
+ # Check for MDL source asset
+ mdl_attr = child.GetAttribute("info:mdl:sourceAsset")
+ if mdl_attr and mdl_attr.Get():
+ mdl_path_val = mdl_attr.Get()
+ if isinstance(mdl_path_val, Sdf.AssetPath) and mdl_path_val.path:
+ mdl_rel = mdl_path_val.path.strip("@")
+
+ # Try to resolve MDL path
+ candidates = [
+ self.usd_dir / mdl_rel,
+ self.usd_dir / "materials" / Path(mdl_rel).name,
+ self.usd_dir / ".." / "materials" / Path(mdl_rel).name,
+ ]
+
+ for c in candidates:
+ if c.exists():
+ return c.resolve()
+
+ return None
+
+ def _get_geomsubset_bindings(
+ self, mesh_prim: Usd.Prim
+ ) -> Dict[str, Tuple[str, List[int]]]:
+ bindings = {}
+
+ for child in mesh_prim.GetChildren():
+ if child.GetTypeName() == "GeomSubset":
+ subset_name = child.GetName()
+
+ # Get face indices for this subset
+ indices_attr = child.GetAttribute("indices")
+ face_indices = (
+ list(indices_attr.Get())
+ if indices_attr and indices_attr.Get()
+ else []
+ )
+
+ # Get material binding
+ mat_path = None
+ binding_rel = child.GetRelationship("material:binding")
+ if binding_rel:
+ targets = binding_rel.GetTargets()
+ if targets:
+ mat_path = str(targets[0])
+
+ if mat_path:
+ bindings[subset_name] = (mat_path, face_indices)
+
+ return bindings
+
+ def extract(self) -> bool:
+ try:
+ self.stage = Usd.Stage.Open(str(self.usd_path))
+ except Exception as e:
+ print(f"ERROR: Could not open USD: {self.usd_path}")
+ print(f" {e}")
+ return False
+
+ if not self.stage:
+ return False
+
+ self._log(f"\n=== Extracting from: {self.usd_path.name} ===")
+
+ # Step 1: Find all materials and extract textures
+ self._log("\n--- Materials ---")
+ for prim in self.stage.Traverse():
+ if prim.GetTypeName() == "Material":
+ mat_path = str(prim.GetPath())
+ self._log(f"\nMaterial: {prim.GetName()}")
+
+ textures = {}
+
+ # Try extracting from shader inputs
+ for child in prim.GetChildren():
+ if child.GetTypeName() == "Shader":
+ shader = UsdShade.Shader(child)
+ textures.update(self._extract_textures_from_shader(shader))
+
+ # If no textures found, try MDL
+ if not textures:
+ mdl_path = self._find_mdl_for_material(prim)
+ if mdl_path:
+ self._log(f" Using MDL: {mdl_path.name}")
+ textures = self._extract_textures_from_mdl(mdl_path)
+
+ # Fallback: if still no textures, search Textures folder for any available
+ if not textures:
+ textures = self._find_fallback_textures(prim.GetName())
+
+ self.materials[mat_path] = textures
+
+ # Step 2: Find all meshes and their material bindings
+ self._log("\n--- Meshes ---")
+ for prim in self.stage.Traverse():
+ if prim.GetTypeName() == "Mesh":
+ mesh_path = str(prim.GetPath())
+ mesh_name = prim.GetName()
+
+ # Get direct material binding first
+ binding_api = UsdShade.MaterialBindingAPI(prim)
+ bound_material = binding_api.ComputeBoundMaterial()[0]
+ mat_path = str(bound_material.GetPath()) if bound_material else None
+
+ # Check for GeomSubset bindings (per-face materials)
+ geomsubset_bindings = self._get_geomsubset_bindings(prim)
+
+ # If no direct binding but has GeomSubsets, use first one as default
+ if not mat_path and geomsubset_bindings:
+ first_subset = list(geomsubset_bindings.values())[0]
+ mat_path = first_subset[0]
+
+ self.mesh_materials[mesh_path] = mat_path
+
+ # Get mesh geometry
+ mesh = UsdGeom.Mesh(prim)
+ points_local = mesh.GetPointsAttr().Get()
+ face_counts = mesh.GetFaceVertexCountsAttr().Get()
+ face_indices = mesh.GetFaceVertexIndicesAttr().Get()
+
+ # Apply world transform to vertices
+ xformable = UsdGeom.Xformable(prim)
+ world_transform = xformable.ComputeLocalToWorldTransform(
+ Usd.TimeCode.Default()
+ )
+
+ # Transform points to world space
+ points = []
+ if points_local:
+ for p in points_local:
+ # Apply 4x4 transform matrix to point
+ p_world = world_transform.Transform(Gf.Vec3d(p[0], p[1], p[2]))
+ points.append(Gf.Vec3f(p_world[0], p_world[1], p_world[2]))
+ else:
+ points = None
+
+ # Get UVs and check interpolation
+ uvs = None
+ uv_interpolation = None
+ uv_indices = None
+ for primvar_name in ["st", "uvs", "uv", "UVMap", "texCoords"]:
+ primvar = UsdGeom.PrimvarsAPI(prim).GetPrimvar(primvar_name)
+ if primvar and primvar.Get():
+ uvs = primvar.Get()
+ uv_interpolation = primvar.GetInterpolation()
+ # For indexed primvars, get the indices
+ if primvar.IsIndexed():
+ uv_indices = primvar.GetIndices()
+ break
+
+ self.meshes[mesh_path] = {
+ "name": mesh_name,
+ "material": mat_path,
+ "points": points,
+ "face_counts": face_counts,
+ "face_indices": face_indices,
+ "uvs": uvs,
+ "uv_interpolation": uv_interpolation,
+ "uv_indices": uv_indices,
+ "geomsubsets": geomsubset_bindings, # Store GeomSubset bindings
+ }
+
+ has_tex = bool(self.materials.get(mat_path))
+ if geomsubset_bindings:
+ has_tex = any(
+ self.materials.get(m) for m, _ in geomsubset_bindings.values()
+ )
+
+ status = "โ" if has_tex else "โ"
+
+ if geomsubset_bindings:
+ self._log(
+ f" {status} {mesh_name} (GeomSubsets: {len(geomsubset_bindings)})"
+ )
+ for subset_name, (sub_mat, _) in geomsubset_bindings.items():
+ sub_mat_name = sub_mat.split("/")[-1] if sub_mat else "none"
+ self._log(f" {subset_name} -> {sub_mat_name}")
+ else:
+ self._log(
+ f" {status} {mesh_name} -> {mat_path or '(no material)'}"
+ )
+
+ if not self.meshes:
+ self._log("WARNING: No meshes found in USD file")
+ return False
+
+ has_valid_mesh = any(
+ mesh_data.get("points") for mesh_data in self.meshes.values()
+ )
+ if not has_valid_mesh:
+ self._log("WARNING: No meshes with valid geometry found")
+ return False
+
+ return True
+
+ def export_obj(
+ self, output_dir: Path, normalize: bool = True
+ ) -> Tuple[Optional[Path], Optional[Path]]:
+ output_dir = Path(output_dir)
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ obj_path = output_dir / "model.obj"
+ mtl_path = output_dir / "model.mtl"
+
+ # Collect all vertices, faces, UVs
+ all_vertices = []
+ all_faces = []
+ all_uvs = []
+ face_materials = []
+ vertex_offset = 0
+ uv_offset = 0
+
+ material_list = [] # List of unique materials used
+ material_map = {} # material_path -> index
+
+ for mesh_path, mesh_data in self.meshes.items():
+ if not mesh_data["points"]:
+ continue
+
+ points = mesh_data["points"]
+ face_counts = mesh_data["face_counts"]
+ face_indices = mesh_data["face_indices"]
+ uvs = mesh_data["uvs"]
+ uv_interpolation = mesh_data.get("uv_interpolation")
+ uv_indices = mesh_data.get("uv_indices")
+ mat_path = mesh_data["material"]
+ geomsubsets = mesh_data.get("geomsubsets", {})
+
+ # Add vertices
+ for p in points:
+ all_vertices.append((float(p[0]), float(p[1]), float(p[2])))
+
+ # Add UVs - handle different interpolation modes
+ mesh_uv_offset = len(all_uvs)
+ if uvs:
+ for uv in uvs:
+ all_uvs.append((float(uv[0]), float(uv[1])))
+
+ # Track UV mapping for this mesh
+ # For faceVarying, we need to map face-vertex index to UV index
+ mesh_data["_uv_offset"] = mesh_uv_offset
+ mesh_data["_has_uvs"] = uvs is not None and len(uvs) > 0
+
+ # Build face-to-material mapping for GeomSubsets
+ face_to_subset_mat = {}
+ if geomsubsets:
+ for subset_name, (
+ sub_mat_path,
+ sub_face_indices,
+ ) in geomsubsets.items():
+ for face_idx in sub_face_indices:
+ face_to_subset_mat[face_idx] = sub_mat_path
+ # Ensure this material is in our list
+ if sub_mat_path and sub_mat_path not in material_map:
+ material_map[sub_mat_path] = len(material_list)
+ material_list.append(sub_mat_path)
+
+ # Track default material
+ if mat_path and mat_path not in material_map:
+ material_map[mat_path] = len(material_list)
+ material_list.append(mat_path)
+
+ # Add faces with proper UV indexing
+ idx = 0
+ face_num = 0
+ face_vertex_idx = 0 # Running index for faceVarying UVs
+
+ for count in face_counts:
+ # Determine material for this face
+ if face_num in face_to_subset_mat:
+ face_mat = face_to_subset_mat[face_num]
+ else:
+ face_mat = mat_path
+
+ mat_idx = material_map.get(face_mat, 0) if face_mat else 0
+
+ # Determine UV indices based on interpolation mode
+ def get_uv_idx(local_vert_idx, fv_offset):
+ if not mesh_data["_has_uvs"]:
+ return None
+ vertex_idx = face_indices[local_vert_idx]
+
+ if uv_interpolation == "faceVarying":
+ if uv_indices is not None:
+ # Indexed faceVarying: indices are per face-vertex
+ return mesh_uv_offset + int(uv_indices[fv_offset])
+ else:
+ # Non-indexed faceVarying: sequential per face-vertex
+ return mesh_uv_offset + fv_offset
+ else:
+ # vertex interpolation
+ if uv_indices is not None:
+ # Indexed vertex: indices are per-vertex
+ return mesh_uv_offset + int(uv_indices[vertex_idx])
+ else:
+ # Non-indexed vertex: UV index matches vertex index
+ return mesh_uv_offset + vertex_idx
+
+ if count == 3:
+ v_indices = [
+ face_indices[idx] + vertex_offset,
+ face_indices[idx + 1] + vertex_offset,
+ face_indices[idx + 2] + vertex_offset,
+ ]
+ uv_idxs = [
+ get_uv_idx(idx, face_vertex_idx),
+ get_uv_idx(idx + 1, face_vertex_idx + 1),
+ get_uv_idx(idx + 2, face_vertex_idx + 2),
+ ]
+ all_faces.append((v_indices, uv_idxs))
+ face_materials.append(mat_idx)
+ face_vertex_idx += 3
+ elif count == 4:
+ # Triangulate quad
+ v_indices1 = [
+ face_indices[idx] + vertex_offset,
+ face_indices[idx + 1] + vertex_offset,
+ face_indices[idx + 2] + vertex_offset,
+ ]
+ v_indices2 = [
+ face_indices[idx] + vertex_offset,
+ face_indices[idx + 2] + vertex_offset,
+ face_indices[idx + 3] + vertex_offset,
+ ]
+ uv_idxs1 = [
+ get_uv_idx(idx, face_vertex_idx),
+ get_uv_idx(idx + 1, face_vertex_idx + 1),
+ get_uv_idx(idx + 2, face_vertex_idx + 2),
+ ]
+ uv_idxs2 = [
+ get_uv_idx(idx, face_vertex_idx),
+ get_uv_idx(idx + 2, face_vertex_idx + 2),
+ get_uv_idx(idx + 3, face_vertex_idx + 3),
+ ]
+ all_faces.append((v_indices1, uv_idxs1))
+ all_faces.append((v_indices2, uv_idxs2))
+ face_materials.append(mat_idx)
+ face_materials.append(mat_idx)
+ face_vertex_idx += 4
+ else:
+ # Skip n-gons
+ face_vertex_idx += count
+
+ idx += count
+ face_num += 1
+
+ vertex_offset += len(points)
+
+ if not all_vertices:
+ return None, None
+
+ # Normalize vertices to fit in [-0.5, 0.5]^3 centered at origin
+ # Use margin factor to ensure object fits fully in camera frame at all angles
+ MARGIN_FACTOR = 0.85 # Scale to 85% of unit cube to leave padding
+
+ if normalize and all_vertices:
+ # Compute bounding box
+ xs = [v[0] for v in all_vertices]
+ ys = [v[1] for v in all_vertices]
+ zs = [v[2] for v in all_vertices]
+
+ min_x, max_x = min(xs), max(xs)
+ min_y, max_y = min(ys), max(ys)
+ min_z, max_z = min(zs), max(zs)
+
+ # Compute center and scale
+ center_x = (min_x + max_x) / 2
+ center_y = (min_y + max_y) / 2
+ center_z = (min_z + max_z) / 2
+
+ extent_x = max_x - min_x
+ extent_y = max_y - min_y
+ extent_z = max_z - min_z
+ max_extent = max(extent_x, extent_y, extent_z)
+
+ if max_extent > 0:
+ # Scale to fit in unit cube, with margin for camera framing
+ scale = MARGIN_FACTOR / max_extent
+ else:
+ scale = 1.0
+
+ # Apply normalization: center then scale
+ all_vertices = [
+ (
+ (v[0] - center_x) * scale,
+ (v[1] - center_y) * scale,
+ (v[2] - center_z) * scale,
+ )
+ for v in all_vertices
+ ]
+
+ self._log(f"\nNormalization applied:")
+ self._log(
+ f" Original bounds: X[{min_x:.2f}, {max_x:.2f}], Y[{min_y:.2f}, {max_y:.2f}], Z[{min_z:.2f}, {max_z:.2f}]"
+ )
+ self._log(f" Scale factor: {scale:.6f} (with {MARGIN_FACTOR:.0%} margin)")
+ self._log(
+ f" Center offset: ({center_x:.2f}, {center_y:.2f}, {center_z:.2f})"
+ )
+
+ # Copy textures and write MTL
+ with open(mtl_path, "w") as f:
+ for mat_path in material_list:
+ mat_name = mat_path.split("/")[-1] if mat_path else "default_material"
+ textures = self.materials.get(mat_path, {})
+
+ f.write(f"newmtl {mat_name}\n")
+ f.write("Ka 0.2 0.2 0.2\n")
+
+ # Use diffuse color constant if available, otherwise default gray
+ if "diffuse_color" in textures:
+ color = textures["diffuse_color"]
+ f.write(f"Kd {color[0]:.6f} {color[1]:.6f} {color[2]:.6f}\n")
+ self._log(
+ f" Material {mat_name}: using diffuse color ({color[0]:.3f}, {color[1]:.3f}, {color[2]:.3f})"
+ )
+ else:
+ f.write("Kd 0.8 0.8 0.8\n")
+
+ f.write("Ks 0.2 0.2 0.2\n")
+ f.write("Ns 50.0\n")
+ f.write("d 1.0\n")
+ f.write("illum 2\n")
+
+ for tex_type, tex_value in textures.items():
+ # Skip color constants (they're tuples, not paths)
+ if isinstance(tex_value, tuple):
+ continue
+
+ tex_path = tex_value
+ if os.path.exists(tex_path):
+ # Copy texture to output dir
+ tex_name = os.path.basename(tex_path)
+ dest = output_dir / tex_name
+ if not dest.exists():
+ shutil.copy2(tex_path, dest)
+
+ # Write to MTL
+ if tex_type == "diffuse":
+ f.write(f"map_Kd {tex_name}\n")
+ elif tex_type == "normal":
+ f.write(f"map_Bump {tex_name}\n")
+ elif tex_type == "roughness":
+ f.write(f"map_Ns {tex_name}\n")
+ elif tex_type == "metallic":
+ f.write(f"map_Ks {tex_name}\n")
+
+ f.write("\n")
+
+ # Write OBJ
+ with open(obj_path, "w") as f:
+ f.write(f"mtllib model.mtl\n\n")
+
+ for v in all_vertices:
+ f.write(f"v {v[0]} {v[1]} {v[2]}\n")
+
+ f.write("\n")
+ for uv in all_uvs:
+ f.write(f"vt {uv[0]} {uv[1]}\n")
+
+ f.write("\n")
+
+ # Group faces by material
+ mat_faces = defaultdict(list)
+ for i, face_data in enumerate(all_faces):
+ mat_faces[face_materials[i]].append(face_data)
+
+ for mat_idx, faces in mat_faces.items():
+ mat_path = (
+ material_list[mat_idx] if mat_idx < len(material_list) else None
+ )
+ mat_name = mat_path.split("/")[-1] if mat_path else "default_material"
+
+ f.write(f"usemtl {mat_name}\n")
+ for face_data in faces:
+ v_indices, uv_indices = face_data
+ # OBJ indices are 1-based
+ if uv_indices[0] is not None:
+ # Include UV indices
+ f.write(
+ f"f {v_indices[0]+1}/{uv_indices[0]+1} {v_indices[1]+1}/{uv_indices[1]+1} {v_indices[2]+1}/{uv_indices[2]+1}\n"
+ )
+ else:
+ # No UVs, just vertex indices
+ f.write(
+ f"f {v_indices[0]+1} {v_indices[1]+1} {v_indices[2]+1}\n"
+ )
+ f.write("\n")
+
+ return obj_path, mtl_path
+
+
+def _install_blender():
+ if not os.path.exists(BLENDER_PATH):
+ os.system("sudo apt-get update")
+ os.system(
+ "sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
+ )
+ os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
+ os.system(
+ f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
+ )
+
+
+def render_usd(
+ usd_path: str,
+ output_dir: str,
+ num_views: int = 150,
+ resolution: int = 512,
+ verbose: bool = False,
+) -> bool:
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Extract mesh and materials from USD
+ extractor = USDMaterialExtractor(usd_path, verbose=verbose)
+ if not extractor.extract():
+ print(f"Failed to extract from USD: {usd_path}")
+ return False
+
+ # Export to OBJ + MTL
+ temp_dir = tempfile.mkdtemp()
+ try:
+ # Don't normalize - let Blender's normalize_scene() handle it
+ obj_path, mtl_path = extractor.export_obj(Path(temp_dir), normalize=False)
+ if not obj_path:
+ print(f"Failed to export OBJ from USD: {usd_path}")
+ return False
+
+ if verbose:
+ print(f"\nExported to: {obj_path}")
+ # List textures
+ textures = list(Path(temp_dir).glob("*.png")) + list(
+ Path(temp_dir).glob("*.tga")
+ )
+ if textures:
+ print(f"Textures copied: {len(textures)}")
+ for t in textures:
+ print(f" - {t.name}")
+
+ # Generate camera views
+ yaws = []
+ pitchs = []
+ offset = (np.random.rand(), np.random.rand())
+ for i in range(num_views):
+ y, p = sphere_hammersley_sequence(i, num_views, offset)
+ yaws.append(y)
+ pitchs.append(p)
+ # Radius 2.5 ensures object corners fit in frame:
+ # Object diagonal at 0.866 from center, visible range at radius=2.5, FOV=40ยฐ is ยฑ0.91
+ radius = [2.1] * num_views
+ fov = [40 / 180 * np.pi] * num_views
+ views = [
+ {"yaw": y, "pitch": p, "radius": r, "fov": f}
+ for y, p, r, f in zip(yaws, pitchs, radius, fov)
+ ]
+
+ # Call Blender
+ blender_script = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
+ "dataset_toolkits",
+ "blender_script",
+ "render.py",
+ )
+
+ args = [
+ BLENDER_PATH,
+ "-b",
+ "-P",
+ blender_script,
+ "--",
+ "--views",
+ json.dumps(views),
+ "--object",
+ str(obj_path),
+ "--resolution",
+ str(resolution),
+ "--output_folder",
+ output_dir,
+ "--engine",
+ "CYCLES",
+ "--save_mesh",
+ "--use_gpu", # Enable GPU acceleration
+ ]
+
+ if verbose:
+ print(f"\nRunning Blender...")
+
+ call(
+ args,
+ stdout=DEVNULL if not verbose else None,
+ stderr=DEVNULL if not verbose else None,
+ )
+
+ success = os.path.exists(os.path.join(output_dir, "transforms.json"))
+ return success
+
+ finally:
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+
+def _render_worker(
+ file_path: str,
+ sha256: str,
+ dataset: str,
+ output_dir: str,
+ num_views: int,
+ quiet: bool,
+) -> Optional[Dict]:
+ output_folder = os.path.join(output_dir, "renders", sha256)
+
+ # Skip if already rendered
+ if os.path.exists(os.path.join(output_folder, "transforms.json")):
+ return {"sha256": sha256, "rendered": True}
+
+ success = render_usd(
+ file_path,
+ output_folder,
+ num_views=num_views,
+ resolution=512,
+ verbose=not quiet,
+ )
+
+ if success:
+ return {"sha256": sha256, "rendered": True}
+ else:
+ if not quiet:
+ print(f"Failed to render: {file_path}")
+ return None
+
+
+def main_batch():
+ import importlib
+ import copy
+ from functools import partial
+ from easydict import EasyDict as edict
+
+ # First argument is dataset type (e.g., "allmats")
+ dataset_utils = importlib.import_module(f"dataset_toolkits.datasets.{sys.argv[1]}")
+
+ parser = argparse.ArgumentParser(
+ description="Batch render USD files with proper texture extraction"
+ )
+ parser.add_argument(
+ "--output_dir", type=str, required=True, help="Directory to save renders"
+ )
+ parser.add_argument(
+ "--filter_low_aesthetic_score",
+ type=float,
+ default=None,
+ help="Filter objects with aesthetic score lower than this value",
+ )
+ parser.add_argument(
+ "--instances",
+ type=str,
+ default=None,
+ help="Instances to process (comma-separated or file path)",
+ )
+ parser.add_argument(
+ "--num_views", type=int, default=150, help="Number of views to render"
+ )
+ parser.add_argument(
+ "--rank", type=int, default=0, help="Worker rank for distributed processing"
+ )
+ parser.add_argument(
+ "--world_size",
+ type=int,
+ default=1,
+ help="Total workers for distributed processing",
+ )
+ parser.add_argument(
+ "--max_workers", type=int, default=8, help="Number of parallel workers"
+ )
+ parser.add_argument("--quiet", action="store_true", help="Suppress verbose output")
+
+ # Add dataset-specific args
+ dataset_utils.add_args(parser)
+
+ opt = parser.parse_args(sys.argv[2:])
+ opt = edict(vars(opt))
+
+ os.makedirs(os.path.join(opt.output_dir, "renders"), exist_ok=True)
+
+ # Install blender
+ if not opt.quiet:
+ print("Checking blender...", flush=True)
+ _install_blender()
+
+ # Get file list from metadata
+ metadata_path = os.path.join(opt.output_dir, "metadata.csv")
+ if not os.path.exists(metadata_path):
+ raise ValueError(f"metadata.csv not found at {metadata_path}")
+
+ metadata = pd.read_csv(metadata_path)
+
+ if opt.instances is None:
+ metadata = metadata[metadata["local_path"].notna()]
+ if opt.filter_low_aesthetic_score is not None:
+ metadata = metadata[
+ metadata["aesthetic_score"] >= opt.filter_low_aesthetic_score
+ ]
+ if "rendered" in metadata.columns:
+ metadata = metadata[metadata["rendered"] == False]
+ else:
+ if os.path.exists(opt.instances):
+ with open(opt.instances, "r") as f:
+ instances = f.read().splitlines()
+ else:
+ instances = opt.instances.split(",")
+ metadata = metadata[metadata["sha256"].isin(instances)]
+
+ # Distributed processing slice
+ start = len(metadata) * opt.rank // opt.world_size
+ end = len(metadata) * (opt.rank + 1) // opt.world_size
+ metadata = metadata[start:end]
+ records = []
+
+ # Filter already processed
+ for sha256 in copy.copy(metadata["sha256"].values):
+ if os.path.exists(
+ os.path.join(opt.output_dir, "renders", sha256, "transforms.json")
+ ):
+ records.append({"sha256": sha256, "rendered": True})
+ metadata = metadata[metadata["sha256"] != sha256]
+
+ print(f"Processing {len(metadata)} objects (rank {opt.rank}/{opt.world_size})...")
+
+ # Process objects
+ from concurrent.futures import ThreadPoolExecutor
+ from tqdm import tqdm
+
+ results = []
+ with ThreadPoolExecutor(max_workers=opt.max_workers) as executor:
+ futures = []
+ for _, row in metadata.iterrows():
+ sha256 = row["sha256"]
+ local_path = row["local_path"]
+ dataset = row.get("dataset", "unknown")
+
+ futures.append(
+ executor.submit(
+ _render_worker,
+ local_path,
+ sha256,
+ dataset,
+ opt.output_dir,
+ opt.num_views,
+ opt.quiet,
+ )
+ )
+
+ for future in tqdm(futures, desc="Rendering", disable=opt.quiet):
+ try:
+ result = future.result()
+ if result is not None:
+ results.append(result)
+ except Exception as e:
+ if not opt.quiet:
+ print(f"Error in worker: {e}")
+
+ # Save results
+ rendered = pd.concat(
+ [pd.DataFrame.from_records(results), pd.DataFrame.from_records(records)]
+ )
+ rendered.to_csv(
+ os.path.join(opt.output_dir, f"rendered_{opt.rank}.csv"), index=False
+ )
+
+ print(f"Done! Rendered {len(results)} objects.")
+
+
+def main_single():
+ parser = argparse.ArgumentParser(
+ description="Render a single USD file with proper texture extraction"
+ )
+ parser.add_argument("usd_file", help="Path to USD file")
+ parser.add_argument(
+ "--output_dir",
+ "-o",
+ default=None,
+ help="Output directory (default: /tmp/render_)",
+ )
+ parser.add_argument(
+ "--num_views", type=int, default=150, help="Number of views to render"
+ )
+ parser.add_argument("--resolution", type=int, default=512, help="Image resolution")
+ parser.add_argument(
+ "--verbose", "-v", action="store_true", help="Print detailed logs"
+ )
+ parser.add_argument(
+ "--extract_only",
+ action="store_true",
+ help="Only extract materials (don't render)",
+ )
+
+ args = parser.parse_args()
+
+ if not os.path.exists(args.usd_file):
+ print(f"ERROR: USD file not found: {args.usd_file}")
+ sys.exit(1)
+
+ if args.extract_only:
+ extractor = USDMaterialExtractor(args.usd_file, verbose=True)
+ extractor.extract()
+
+ print("\n=== SUMMARY ===")
+ print(f"Meshes: {len(extractor.meshes)}")
+ print(f"Materials: {len(extractor.materials)}")
+
+ total_textures = sum(len(t) for t in extractor.materials.values())
+ print(f"Total textures: {total_textures}")
+
+ for mat_path, textures in extractor.materials.items():
+ if textures:
+ mat_name = mat_path.split("/")[-1] if mat_path else "unknown"
+ print(f"\n{mat_name}:")
+ for tex_type, tex_path in textures.items():
+ print(f" {tex_type}: {os.path.basename(tex_path)}")
+ else:
+ _install_blender()
+
+ output_dir = args.output_dir
+ if not output_dir:
+ filename = Path(args.usd_file).stem
+ output_dir = f"/tmp/render_{filename}"
+
+ success = render_usd(
+ args.usd_file,
+ output_dir,
+ num_views=args.num_views,
+ resolution=args.resolution,
+ verbose=args.verbose,
+ )
+
+ if success:
+ print(f"\nโ Rendered to: {output_dir}")
+ else:
+ print(f"\nโ Rendering failed")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ # Check if first arg is a dataset type (batch mode) or a file (single mode)
+ if (
+ len(sys.argv) > 1
+ and not sys.argv[1].startswith("-")
+ and not os.path.exists(sys.argv[1])
+ ):
+ # Batch mode: first arg is dataset type like "allmats"
+ main_batch()
+ else:
+ # Single file mode
+ main_single()
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/commercial.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/commercial.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a381c7a040d928ae2e7403365805ba07b777d0c
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/commercial.py
@@ -0,0 +1,427 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ COMMERCIAL_BASE_DIR,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
+ render_sphere_with_texture,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
+ analyze_material_with_vlm,
+ parse_vlm_properties,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ extract_materials_from_usd,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.residential import (
+ PROMPTS,
+ make_user_prompt,
+)
+import re
+from tqdm import tqdm
+import os
+import logging
+import copy
+
+# Use the centralized parser function
+parse_vlm_output = parse_vlm_properties
+
+
+def list_commercial_objects():
+ """
+ List all available commercial objects in the commercial directory.
+ """
+ usd_files = []
+ print("\nAvailable commercial objects:")
+ for root, _, files in os.walk(COMMERCIAL_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_files.append(os.path.join(root, file))
+ print(f" - {os.path.basename(root)}/{file}")
+ print()
+
+
+def process_commercial(
+ vlm_model,
+ vlm_processor,
+ limit=None,
+ processed_objects=None,
+ output_file=None,
+ existing_results=None,
+):
+ usd_files = []
+ for root, _, files in os.walk(COMMERCIAL_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_files.append(os.path.join(root, file))
+
+ logging.info(f"Found {len(usd_files)} USD files in commercial dataset")
+
+ # Initialize tracking sets and results
+ processed_objects = set() if processed_objects is None else processed_objects
+ existing_results = [] if existing_results is None else existing_results
+
+ # Build a set of already processed object names from existing_results
+ existing_object_names = {
+ result.get("object_name")
+ for result in existing_results
+ if "object_name" in result
+ }
+ logging.info(
+ f"Found {len(existing_object_names)} already processed objects in existing results"
+ )
+
+ # Add names from existing_results to processed_objects to avoid reprocessing
+ processed_objects.update(existing_object_names)
+
+ # Create a copy of existing_results to avoid modifying the original
+ all_results = copy.deepcopy(existing_results)
+
+ usd_files.sort()
+
+ if limit and limit > 0:
+ usd_files = usd_files[:limit]
+
+ success_count = 0
+ failed_objects = []
+ total_segments = 0
+ unique_materials = set()
+ materials_per_object = {}
+ total_rendered_segments = 0
+ total_vlm_segments = 0
+
+ # Count total segments from existing results
+ for result in existing_results:
+ total_segments += len(result.get("segments", {}))
+
+ # Statistics for texture availability
+ segments_with_texture = 0
+ segments_without_texture = 0
+ segments_with_thumbnail_only = 0
+
+ # Track processed files to avoid duplicates from the same directory
+ processed_files = set()
+
+ for usd_file in tqdm(usd_files, desc=f"Processing commercial dataset"):
+ # Extract object name from path
+ object_name = os.path.basename(os.path.dirname(usd_file))
+
+ # Skip if we already processed this exact file
+ if usd_file in processed_files:
+ continue
+
+ # Skip objects that have already been processed
+ if object_name in processed_objects:
+ logging.info(f"Skipping already processed object: {object_name}")
+ continue
+
+ try:
+ directory = os.path.dirname(usd_file)
+
+ # Extract material information
+ result = extract_materials_from_usd(usd_file, "commercial")
+
+ if result:
+ # Add to processed_files to avoid duplicates
+ processed_files.add(usd_file)
+
+ # Track statistics
+ segments = result.get("segments", {})
+ total_segments += len(segments)
+
+ # Remove object_name and note fields from segments
+ for segment_key, segment_info in segments.items():
+ if "object_name" in segment_info:
+ del segment_info["object_name"]
+ if "note" in segment_info:
+ del segment_info["note"]
+
+ # Count unique materials for this object
+ object_materials = set()
+ for segment_name, segment_info in segments.items():
+ material_name = segment_info.get("material_type", "unknown")
+ unique_materials.add(material_name)
+ object_materials.add(material_name)
+
+ # Record materials per object
+ if len(segments) > 0:
+ materials_per_object[object_name] = len(object_materials)
+
+ # Get thumbnail path if available
+ thumb_path = None
+ # For commercial dataset, thumbnails are in .thumbs/256x256 directory
+ thumb_dir = os.path.join(
+ os.path.dirname(usd_file), ".thumbs", "256x256"
+ )
+
+ has_thumbnail = False
+ if os.path.exists(thumb_dir):
+ # Try to find a thumbnail matching the USD filename
+ usd_filename = os.path.basename(usd_file)
+ thumb_candidates = [
+ # Regular thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.png"),
+ # Auto-generated thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.auto.png"),
+ ]
+
+ for candidate in thumb_candidates:
+ if os.path.exists(candidate):
+ thumb_path = candidate
+ has_thumbnail = True
+ logging.info(f"Found thumbnail: {thumb_path}")
+ break
+
+ # Process VLM for all segments if VLM model is provided
+ os.makedirs("/tmp/vlm", exist_ok=True)
+
+ if vlm_model and len(segments) > 0:
+ for segment_key, segment_info in segments.items():
+ textures = segment_info.get("textures", {})
+
+ # Log texture information for diagnostics
+ logging.info(
+ f"Segment {segment_key} has textures: {list(textures.keys())}"
+ )
+
+ # Check if we have either a normal or roughness texture for rendering
+ has_texture = (
+ "normal" in textures
+ or "roughness" in textures
+ or "diffuse" in textures
+ )
+ if has_texture:
+ # Has texture - render sphere and use with thumbnail
+ segments_with_texture += 1
+ logging.info(
+ f"Rendering texture sphere for {object_name}, segment {segment_key}"
+ )
+
+ # Set up file path for this segment's rendered sphere
+ segment_render_path = f"/tmp/vlm/texture_sphere_{object_name}_{segment_key}.png"
+
+ # Render the textured sphere
+ try:
+ rgb_buffer = render_sphere_with_texture(
+ textures, segment_render_path
+ )
+ logging.info(f"RGB buffer shape: {rgb_buffer.shape}")
+ except Exception as e:
+ logging.error(
+ f"Error rendering texture for {segment_key}: {str(e)}"
+ )
+ segment_render_path = None
+ else:
+ # No texture - just use thumbnail
+ segments_without_texture += 1
+ segment_render_path = None
+ logging.info(
+ f"No texture for {object_name}, segment {segment_key}. Using thumbnail only."
+ )
+
+ # Always try to process with VLM, even if no texture
+ try:
+ # If we have a thumbnail but no texture, still run VLM with just the thumbnail
+ if not has_texture and has_thumbnail:
+ segments_with_thumbnail_only += 1
+ logging.info(
+ f"Using thumbnail only for {object_name}, segment {segment_key}"
+ )
+
+ # Don't run VLM if we have neither texture nor thumbnail
+ if not segment_render_path and not has_thumbnail:
+ logging.warning(
+ f"Skipping VLM for {segment_key} - no texture or thumbnail available"
+ )
+ continue
+
+ # Set semantic usage to segment name but don't store in segment data
+ semantic_usage = segment_key
+ temp_object_name = object_name
+
+ # Create custom prompt based on texture availability
+ part1 = make_user_prompt(
+ segment_info["material_type"],
+ semantic_usage,
+ temp_object_name,
+ has_texture_sphere=segment_render_path is not None,
+ )
+
+ # Store the custom prompt in material_info but not object_name
+ segment_info["user_prompt"] = part1
+
+ # Debug: Log the prompt type based on texture availability
+ if segment_render_path is not None:
+ logging.info(
+ f"Using prompt WITH texture sphere for {object_name}, segment {segment_key}"
+ )
+ else:
+ logging.info(
+ f"Using prompt WITHOUT texture sphere for {object_name}, segment {segment_key}"
+ )
+ logging.info(
+ f"PROMPT: {part1[:100]}..."
+ ) # Print just the beginning of the prompt
+
+ # Create a temporary segment_info with object_name for VLM but don't save to result
+ temp_segment_info = segment_info.copy()
+ temp_segment_info["semantic_usage"] = semantic_usage
+ temp_segment_info["object_name"] = temp_object_name
+
+ vlm_analysis = analyze_material_with_vlm(
+ segment_render_path, # This can be None, in which case only thumbnail is used
+ temp_segment_info, # Use temporary copy with object_name
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name="commercial",
+ PROMPTS=PROMPTS,
+ make_user_prompt=make_user_prompt,
+ parse_vlm_output=parse_vlm_output,
+ )
+
+ # Add VLM analysis to segment info
+ if vlm_analysis and "error" not in vlm_analysis:
+ segment_info["vlm_analysis"] = vlm_analysis.get(
+ "vlm_analysis"
+ )
+
+ if vlm_analysis.get("youngs_modulus") is not None:
+ segment_info["youngs_modulus"] = vlm_analysis.get(
+ "youngs_modulus"
+ )
+
+ if vlm_analysis.get("poissons_ratio") is not None:
+ segment_info["poissons_ratio"] = vlm_analysis.get(
+ "poissons_ratio"
+ )
+
+ if vlm_analysis.get("density") is not None:
+ segment_info["density"] = vlm_analysis.get(
+ "density"
+ )
+
+ total_vlm_segments += 1
+ logging.info(
+ f"VLM analysis successful for {segment_key}:"
+ )
+ logging.info(
+ f" Young's modulus: {vlm_analysis.get('youngs_modulus')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {vlm_analysis.get('poissons_ratio')}"
+ )
+ logging.info(
+ f" Density: {vlm_analysis.get('density')}"
+ )
+ else:
+ logging.error(
+ f"VLM analysis failed for {segment_key}: {vlm_analysis.get('error', 'Unknown error')}"
+ )
+ except Exception as e:
+ import traceback
+
+ logging.error(
+ f"Error during VLM analysis for {segment_key}: {str(e)}"
+ )
+ logging.error(traceback.format_exc())
+
+ total_rendered_segments += 1
+
+ all_results.append(result) # Add to our local copy of results
+ processed_objects.add(object_name) # Mark as processed
+
+ # Incremental save after each object if output file is provided
+ if output_file:
+ try:
+ with open(output_file, "w") as f:
+ import json
+ from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ UsdJsonEncoder,
+ )
+
+ # Debug save contents
+ logging.info(
+ f"Saving checkpoint with {len(all_results)} objects"
+ )
+
+ # Ensure result types are JSON serializable
+ for idx, item in enumerate(all_results):
+ if "segments" in item:
+ for seg_key, seg_info in item["segments"].items():
+ # Remove object_name and note fields if they exist
+ if "object_name" in seg_info:
+ del seg_info["object_name"]
+ if "note" in seg_info:
+ del seg_info["note"]
+
+ if "textures" in seg_info and isinstance(
+ seg_info["textures"], dict
+ ):
+ # Convert any non-serializable texture paths to strings
+ serializable_textures = {}
+ for tex_type, tex_path in seg_info[
+ "textures"
+ ].items():
+ serializable_textures[tex_type] = str(
+ tex_path
+ )
+ seg_info["textures"] = serializable_textures
+
+ # Dump to file
+ json.dump(all_results, f, indent=4, cls=UsdJsonEncoder)
+
+ except Exception as e:
+ logging.error(f"Error saving checkpoint: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+
+ success_count += 1
+ else:
+ logging.warning(f"No material information extracted for {usd_file}")
+ failed_objects.append(object_name)
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error processing {usd_file}: {str(e)}")
+ logging.error(traceback.format_exc())
+ failed_objects.append(os.path.basename(os.path.dirname(usd_file)))
+
+ # Log texture statistics
+ logging.info("Texture Statistics:")
+ logging.info(f" Total segments processed: {total_segments}")
+ logging.info(f" Segments with textures: {segments_with_texture}")
+ logging.info(f" Segments without textures: {segments_without_texture}")
+ logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
+ logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
+
+ # Convert materials_per_object to list format for consistency with simready
+ materials_per_object_list = []
+ for obj_name, count in materials_per_object.items():
+ materials_per_object_list.append(obj_name)
+
+ return (
+ all_results,
+ len(usd_files),
+ success_count,
+ failed_objects,
+ total_segments,
+ total_rendered_segments,
+ total_vlm_segments,
+ list(unique_materials),
+ materials_per_object_list,
+ )
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/common.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..05aa06fe06ddcc9ff09f0c97845efee3040ef647
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/common.py
@@ -0,0 +1,1457 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import json
+import glob
+import argparse
+import numpy as np
+import logging
+import copy
+from PIL import Image
+from tqdm import tqdm
+from pxr import Usd, UsdGeom, UsdShade, Sdf, Ar
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ COMMERCIAL_BASE_DIR,
+ RESIDENTIAL_BASE_DIR,
+ VEGETATION_BASE_DIR,
+)
+import datetime
+import uuid
+
+
+class UsdJsonEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if hasattr(obj, "__dict__"):
+ return obj.__dict__
+ return str(obj)
+
+
+def find_textures_for_material(object_dir, texture_path):
+ """
+ Find textures referenced by a material in a USD file.
+
+ Args:
+ object_dir (str): Directory containing the USD file
+ texture_path (str): Texture path from the USD file
+
+ Returns:
+ dict: Dictionary mapping texture types to full paths
+ """
+ if not texture_path:
+ return {}
+
+ # Convert Sdf.AssetPath to string if needed
+ if (
+ hasattr(texture_path, "__class__")
+ and texture_path.__class__.__name__ == "AssetPath"
+ ):
+ texture_path = str(texture_path)
+
+ # Handle absolute paths
+ if os.path.isabs(texture_path):
+ if os.path.exists(texture_path):
+ return {determine_texture_type(texture_path): texture_path}
+ return {}
+
+ # Handle relative paths with various prefixes
+ clean_path = texture_path.replace("@", "").replace("./", "")
+
+ # Try direct path
+ direct_path = os.path.join(object_dir, clean_path)
+ if os.path.exists(direct_path):
+ return {determine_texture_type(direct_path): direct_path}
+
+ # Try common texture directories
+ texture_dirs = []
+ for texture_dir_name in [
+ "textures",
+ "Textures",
+ "materials/textures",
+ "Materials/Textures",
+ ]:
+ texture_dir = os.path.join(object_dir, texture_dir_name)
+ if os.path.isdir(texture_dir):
+ texture_dirs.append(texture_dir)
+
+ # Look in parent directory if object_dir doesn't have textures
+ if not texture_dirs:
+ parent_dir = os.path.dirname(object_dir)
+ for texture_dir_name in [
+ "textures",
+ "Textures",
+ "materials/textures",
+ "Materials/Textures",
+ ]:
+ texture_dir = os.path.join(parent_dir, texture_dir_name)
+ if os.path.isdir(texture_dir):
+ texture_dirs.append(texture_dir)
+
+ # Check for texture in each texture directory
+ for texture_dir in texture_dirs:
+ texture_file = os.path.join(texture_dir, os.path.basename(clean_path))
+ if os.path.exists(texture_file):
+ return {determine_texture_type(texture_file): texture_file}
+
+ return {}
+
+
+def determine_texture_type(texture_path):
+ """
+ Determine the type of texture based on its filename.
+
+ Args:
+ texture_path (str): Path to texture file
+
+ Returns:
+ str: Texture type (albedo, normal, roughness, metallic, orm)
+ """
+ filename = os.path.basename(texture_path).lower()
+
+ # Check for common texture type indicators in filename
+ if any(
+ term in filename
+ for term in ["albedo", "basecolor", "color", "_a.", "_a_", "_diffuse", "_diff"]
+ ):
+ return "albedo"
+ elif any(term in filename for term in ["normal", "nrm", "_n.", "_n_"]):
+ return "normal"
+ elif any(term in filename for term in ["roughness", "rough", "_r.", "_r_"]):
+ return "roughness"
+ elif any(term in filename for term in ["metallic", "metal", "_m.", "_m_"]):
+ return "metallic"
+ elif any(term in filename for term in ["orm", "arm", "occlusion"]):
+ return "orm"
+ elif any(term in filename for term in ["emissive", "emission", "_e."]):
+ return "emissive"
+ elif any(term in filename for term in ["opacity", "transparent", "alpha"]):
+ return "opacity"
+ elif any(term in filename for term in ["specular", "spec", "_s."]):
+ return "specular"
+ elif any(term in filename for term in ["displacement", "height", "bump"]):
+ return "displacement"
+
+ # If no specific type is identified, make an educated guess based on file extension
+ ext = os.path.splitext(filename)[1].lower()
+ if ext in [".jpg", ".jpeg", ".png", ".tga", ".tif", ".tiff"]:
+ return "albedo" # Default to albedo for unrecognized image files
+
+ return "unknown"
+
+
+def copy_texture_to_output(
+ texture_path, output_dir, object_name, material_name, texture_type
+):
+ """
+ Copy a texture file to the output directory with a standardized name.
+
+ Args:
+ texture_path (str): Source texture path
+ output_dir (str): Output directory
+ object_name (str): Name of the object
+ material_name (str): Name of the material
+ texture_type (str): Type of texture
+
+ Returns:
+ str: Path to the copied texture file
+ """
+ if not os.path.exists(texture_path):
+ return None
+
+ # Create output subdirectory for this object if it doesn't exist
+ object_output_dir = os.path.join(output_dir, object_name)
+ os.makedirs(object_output_dir, exist_ok=True)
+
+ # Create standardized output filename
+ texture_ext = os.path.splitext(texture_path)[1]
+ output_filename = f"{material_name}_{texture_type}{texture_ext}"
+ output_path = os.path.join(object_output_dir, output_filename)
+
+ try:
+ # Copy the texture file
+ import shutil
+
+ shutil.copy2(texture_path, output_path)
+ return output_path
+ except Exception as e:
+ logging.error(f"Error copying texture {texture_path}: {str(e)}")
+ return None
+
+
+def extract_material_from_shader(shader_prim, object_dir, dataset_type=None):
+ """
+ Extract material properties and textures from a shader prim.
+
+ Args:
+ shader_prim (UsdShade.Shader): Shader prim
+ object_dir (str): Directory containing the USD file
+ dataset_type (str, optional): Type of dataset (commercial, residential, vegetation)
+
+ Returns:
+ dict: Dictionary with material properties and textures
+ """
+ material_info = {"textures": {}}
+
+ # Create a shader object from the prim
+ shader = UsdShade.Shader(shader_prim)
+ if not shader:
+ logging.warning(f"Failed to create shader from {shader_prim.GetPath()}")
+ return material_info
+
+ # Get material name from shader path
+ shader_path = str(shader_prim.GetPath())
+ material_name = None
+ if "/Looks/" in shader_path:
+ material_name = shader_path.split("/Looks/")[1].split("/")[0]
+
+ logging.info(f"Processing shader for material: {material_name}")
+
+ # For vegetation materials, try to find matching textures by material name
+ if dataset_type == "vegetation" and material_name:
+ # Find the materials/textures directory
+ object_dir_parts = object_dir.split(os.sep)
+ trees_dir = None
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "Trees":
+ trees_dir = os.sep.join(object_dir_parts[: i + 1])
+ break
+
+ if trees_dir:
+ textures_dir = os.path.join(trees_dir, "materials", "textures")
+ if os.path.exists(textures_dir):
+ material_name_lower = material_name.lower()
+ material_parts = material_name_lower.replace("_", " ").split()
+
+ # Get all texture files in the directory
+ texture_files = [
+ f
+ for f in os.listdir(textures_dir)
+ if f.lower().endswith((".png", ".jpg", ".jpeg", ".tif", ".tiff"))
+ ]
+
+ # Track potential matches for different texture types
+ texture_matches = {
+ "diffuse": [],
+ "normal": [],
+ "roughness": [],
+ "metallic": [],
+ "orm": [],
+ }
+
+ # Categorize material into types
+ material_categories = {
+ "bark": [
+ "bark",
+ "trunk",
+ "wood",
+ "tree",
+ "log",
+ "stump",
+ "stem",
+ "branch",
+ "twig",
+ ],
+ "leaf": ["leaf", "leaves", "foliage", "needle", "needles", "frond"],
+ "flower": [
+ "flower",
+ "flowers",
+ "petal",
+ "petals",
+ "bloom",
+ "blossom",
+ ],
+ "fruit": [
+ "fruit",
+ "fruits",
+ "berry",
+ "berries",
+ "seed",
+ "seeds",
+ "cone",
+ "cones",
+ ],
+ "grass": [
+ "grass",
+ "grasses",
+ "reed",
+ "reeds",
+ "sedge",
+ "rush",
+ "blade",
+ ],
+ }
+
+ # Find all applicable categories
+ material_types = []
+ for category, keywords in material_categories.items():
+ if any(keyword in material_name_lower for keyword in keywords):
+ material_types.append(category)
+
+ # If we couldn't determine a category from material name, try from object name
+ if not material_types:
+ object_name = os.path.splitext(os.path.basename(object_dir))[
+ 0
+ ].lower()
+ for category, keywords in material_categories.items():
+ if any(keyword in object_name for keyword in keywords):
+ material_types.append(category)
+
+ # Still no category? Add generic fallbacks
+ if not material_types:
+ # Default to bark for most vegetation models
+ material_types = ["bark"]
+
+ logging.info(
+ f"Material categories for {material_name}: {material_types}"
+ )
+
+ # Scoring function for texture relevance to material name
+ def score_texture_for_material(texture_name, texture_type):
+ score = 0
+ texture_name_lower = texture_name.lower()
+
+ # Direct material name match (highest priority)
+ if material_name_lower in texture_name_lower:
+ score += 200
+
+ # Match individual parts of material name
+ for part in material_parts:
+ if len(part) > 2 and part in texture_name_lower:
+ score += 50
+
+ # Match material categories
+ for material_type in material_types:
+ # Match exact category name
+ if material_type in texture_name_lower:
+ score += 100
+
+ # Match keywords for this category
+ for keyword in material_categories.get(material_type, []):
+ if keyword in texture_name_lower:
+ score += 40
+
+ # Correct type suffix
+ type_suffixes = {
+ "diffuse": [
+ "basecolor",
+ "albedo",
+ "color",
+ "diffuse",
+ "_bc",
+ "_a",
+ "_d",
+ ],
+ "normal": ["normal", "nrm", "_n", "nor"],
+ "roughness": ["roughness", "rough", "_r", "rgh"],
+ "metallic": ["metallic", "metal", "_m", "mtl"],
+ "orm": ["orm", "arm", "occlusion"],
+ }
+
+ for suffix in type_suffixes.get(texture_type, []):
+ if suffix in texture_name_lower:
+ score += 40
+
+ # Boost score for more specific matches (longer texture names probably more specific)
+ if len(texture_name_lower) > 15:
+ score += 10
+
+ # Exact matches for specific materials
+ if material_name_lower == "bark" and "bark" in texture_name_lower:
+ score += 50
+ elif (
+ material_name_lower == "leaves" and "leaf" in texture_name_lower
+ ):
+ score += 50
+ elif (
+ material_name_lower == "needle"
+ and "needle" in texture_name_lower
+ ):
+ score += 50
+ elif (
+ "trunk" in material_name_lower and "bark" in texture_name_lower
+ ):
+ score += 30
+
+ return score
+
+ # Process each texture file
+ for texture_file in texture_files:
+ # Determine texture type
+ texture_type = determine_texture_type(texture_file)
+
+ # Don't process "unknown" textures
+ if texture_type == "unknown":
+ continue
+
+ # Score this texture for this material
+ score = score_texture_for_material(texture_file, texture_type)
+
+ # If it's a good match (score > 0), add to potential matches
+ if score > 0:
+ # Convert diffuse type to match our expected naming
+ if texture_type in ["albedo", "basecolor", "color"]:
+ texture_type = "diffuse"
+
+ # Add to matches with score
+ if texture_type in texture_matches:
+ texture_matches[texture_type].append((texture_file, score))
+
+ # Sort matches by score and select the best for each type
+ for texture_type, matches in texture_matches.items():
+ if matches:
+ # Sort by score (highest first)
+ matches.sort(key=lambda x: x[1], reverse=True)
+ best_match = matches[0][0]
+
+ # Add to material info
+ texture_path = os.path.join(textures_dir, best_match)
+ material_info["textures"][texture_type] = texture_path
+ logging.info(
+ f"Found {texture_type} texture for {material_name}: {best_match}"
+ )
+
+ # If we still don't have textures, use fallbacks from generic categories
+ if not any(material_info["textures"].values()):
+ logging.info(
+ f"No direct texture matches found for {material_name}, trying category fallbacks"
+ )
+
+ # Key textures we need
+ needed_types = ["diffuse", "normal", "roughness"]
+
+ # Generic fallbacks by category
+ fallbacks = {
+ "bark": {
+ "diffuse": "pinebark1_basecolor.png",
+ "normal": "pinebark1_normal.png",
+ "roughness": "pinebark1_roughness.png",
+ },
+ "leaf": {
+ "diffuse": "oakleaves1_basecolor.png",
+ "normal": "oakleaves1_normal.png",
+ "roughness": "oakleaves1_roughness.png",
+ },
+ "flower": {
+ "diffuse": "goldenchain_flowers_basecolor.png",
+ "normal": "goldenchain_flowers_normal.png",
+ "roughness": "goldenchain_flowers_roughness.png",
+ },
+ "grass": {
+ "diffuse": "ashleaves1_basecolor.png",
+ "normal": "ashleaves1_normal.png",
+ "roughness": "ashleaves1_roughness.png",
+ },
+ "needle": {
+ "diffuse": "spruceneedles_basecolor.png",
+ "normal": "spruceneedles_normal.png",
+ "roughness": "spruceneedles_roughness.png",
+ },
+ }
+
+ # Try each category we matched
+ for material_type in material_types:
+ if material_type in fallbacks:
+ for texture_type in needed_types:
+ if texture_type not in material_info[
+ "textures"
+ ] and fallbacks[material_type].get(texture_type):
+ fallback_file = fallbacks[material_type][
+ texture_type
+ ]
+ fallback_path = os.path.join(
+ textures_dir, fallback_file
+ )
+ if os.path.exists(fallback_path):
+ material_info["textures"][
+ texture_type
+ ] = fallback_path
+ logging.info(
+ f"Using fallback {texture_type} texture for {material_name}: {fallback_file}"
+ )
+
+ # If still missing textures, use bark as an ultimate fallback (most common)
+ for texture_type in needed_types:
+ if texture_type not in material_info["textures"]:
+ fallback_file = fallbacks["bark"][texture_type]
+ fallback_path = os.path.join(textures_dir, fallback_file)
+ if os.path.exists(fallback_path):
+ material_info["textures"][texture_type] = fallback_path
+ logging.info(
+ f"Using ultimate fallback {texture_type} texture for {material_name}: {fallback_file}"
+ )
+
+ # Check for shader attributes
+ inputs_to_check = [
+ # Common texture inputs
+ "diffuse_color_texture",
+ "inputs:diffuse_color_texture",
+ "normalmap_texture",
+ "inputs:normalmap_texture",
+ "reflectionroughness_texture",
+ "inputs:reflectionroughness_texture",
+ "diffusecolor_texture",
+ "inputs:diffusecolor_texture",
+ "normal_texture",
+ "inputs:normal_texture",
+ "roughness_texture",
+ "inputs:roughness_texture",
+ # Common material constants
+ "diffuse_color_constant",
+ "inputs:diffuse_color_constant",
+ "reflection_roughness_constant",
+ "inputs:reflection_roughness_constant",
+ "metallic_constant",
+ "inputs:metallic_constant",
+ "opacity_constant",
+ "inputs:opacity_constant",
+ "emissive_color_constant",
+ "inputs:emissive_color_constant",
+ ]
+
+ # Process each input attribute
+ for input_name in inputs_to_check:
+ # Remove "inputs:" prefix if present
+ input_name_clean = input_name.replace("inputs:", "")
+
+ # Try to get the input
+ shader_input = shader.GetInput(input_name_clean)
+ if not shader_input:
+ continue
+
+ # Get the value
+ value = shader_input.Get()
+ if value is None:
+ continue
+
+ # Format input name to standard form
+ standard_name = input_name_clean.lower()
+
+ # Check if this is a texture input
+ if "texture" in standard_name:
+ # Determine texture type
+ if "normal" in standard_name:
+ texture_type = "normal"
+ elif "rough" in standard_name:
+ texture_type = "roughness"
+ elif "diffuse" in standard_name or "color" in standard_name:
+ texture_type = "diffuse"
+ elif "specular" in standard_name:
+ texture_type = "specular"
+ elif "metallic" in standard_name:
+ texture_type = "metallic"
+ elif "opacity" in standard_name:
+ texture_type = "opacity"
+ elif "emissive" in standard_name:
+ texture_type = "emissive"
+ else:
+ texture_type = "other"
+
+ # Handle asset path values
+ if isinstance(value, Sdf.AssetPath):
+ texture_path = value.resolvedPath
+ if not texture_path:
+ # Try to resolve relative path
+ rel_path = value.path
+ if rel_path.startswith("./"):
+ rel_path = rel_path[2:]
+ texture_path = os.path.join(object_dir, rel_path)
+
+ if os.path.exists(texture_path):
+ # If we already found a texture through our material name matching,
+ # don't override it for vegetation materials
+ if (
+ dataset_type == "vegetation"
+ and texture_type in material_info["textures"]
+ ):
+ logging.info(
+ f"Keeping already found {texture_type} texture for {material_name}"
+ )
+ else:
+ material_info["textures"][texture_type] = texture_path
+
+ # For vegetation, try to find exact textures by material name
+ if (
+ dataset_type == "vegetation"
+ and not material_info["textures"].get(texture_type)
+ and material_name
+ ):
+ logging.info(
+ f"Looking for exact vegetation texture: {texture_type} for {material_name}"
+ )
+
+ # Find the materials/textures directory
+ object_dir_parts = object_dir.split(os.sep)
+ trees_dir = None
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "Trees":
+ trees_dir = os.sep.join(object_dir_parts[: i + 1])
+ break
+
+ if trees_dir:
+ materials_dir = os.path.join(trees_dir, "materials")
+ textures_dir = os.path.join(materials_dir, "textures")
+
+ logging.info(f"Looking for textures in: {textures_dir}")
+
+ if os.path.exists(textures_dir):
+ # Look for textures with material name
+ material_name_lower = material_name.lower()
+
+ # Build specific patterns for this material name
+ specific_patterns = [
+ f"{material_name_lower}_{texture_type}.png",
+ f"{material_name_lower.replace('_', '')}_{texture_type}.png",
+ ]
+
+ # Try alternate texture type names for diffuse
+ if texture_type == "diffuse":
+ specific_patterns.extend(
+ [
+ f"{material_name_lower}_basecolor.png",
+ f"{material_name_lower.replace('_', '')}_basecolor.png",
+ f"{material_name_lower}_albedo.png",
+ f"{material_name_lower.replace('_', '')}_albedo.png",
+ ]
+ )
+
+ # Search for exact matches only
+ for pattern in specific_patterns:
+ potential_file = os.path.join(textures_dir, pattern)
+ if os.path.exists(potential_file):
+ logging.info(
+ f"Found exact vegetation texture: {os.path.basename(potential_file)}"
+ )
+ material_info["textures"][texture_type] = potential_file
+ break
+
+ # If exact match not found, try partial matches
+ if not material_info["textures"].get(texture_type):
+ for file in os.listdir(textures_dir):
+ file_lower = file.lower()
+ if (
+ file_lower.endswith(".png")
+ and material_name_lower in file_lower
+ ):
+ # Check for texture type in filename
+ if texture_type in file_lower or (
+ texture_type == "diffuse"
+ and "basecolor" in file_lower
+ ):
+ full_path = os.path.join(textures_dir, file)
+ logging.info(
+ f"Found related vegetation texture: {file}"
+ )
+ material_info["textures"][
+ texture_type
+ ] = full_path
+ break
+ else:
+ # Handle non-texture attributes
+ material_info[standard_name] = value
+
+ return material_info
+
+
+def apply_generic_textures_to_segments(
+ segments, object_name, object_dir, output_textures_dir=None
+):
+ """
+ Apply generic textures to mesh segments that don't have textures.
+
+ Args:
+ segments (dict): Segments dictionary to update
+ object_name (str): Name of the object
+ object_dir (str): Directory containing the USD file
+ output_textures_dir (str, optional): Directory to save extracted textures
+
+ Returns:
+ dict: Updated segments dictionary
+ """
+ # Skip if no segments
+ if not segments:
+ return segments
+
+ # Find the materials/textures directory
+ object_dir_parts = object_dir.split(os.sep)
+ trees_dir = None
+ shrub_dir = None
+ debris_dir = None
+
+ # Look for Trees directory
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "Trees":
+ trees_dir = os.sep.join(object_dir_parts[: i + 1])
+ break
+
+ # Look for Shrub directory
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "Shrub":
+ shrub_dir = os.sep.join(object_dir_parts[: i + 1])
+ break
+
+ # Look for Debris directory
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "Debris":
+ debris_dir = os.sep.join(object_dir_parts[: i + 1])
+ break
+
+ # Set up textures directory based on dataset subdirectory found
+ if trees_dir:
+ textures_dir = os.path.join(trees_dir, "materials", "textures")
+ elif shrub_dir:
+ textures_dir = os.path.join(shrub_dir, "materials", "textures")
+ elif debris_dir:
+ textures_dir = os.path.join(debris_dir, "materials", "textures")
+ else:
+ # Check for Plant_Tropical directory
+ tropical_dir = None
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "Plant_Tropical":
+ tropical_dir = os.sep.join(object_dir_parts[: i + 1])
+ break
+
+ if tropical_dir:
+ textures_dir = os.path.join(tropical_dir, "materials", "textures")
+ else:
+ # Try looking for material textures directory in current location
+ textures_dir = os.path.join(object_dir, "materials", "textures")
+ if not os.path.exists(textures_dir):
+ # Go up one directory and look there
+ parent_dir = os.path.dirname(object_dir)
+ textures_dir = os.path.join(parent_dir, "materials", "textures")
+ if not os.path.exists(textures_dir):
+ # Try root vegetation directory as a last resort
+ veg_root = None
+ for i in range(len(object_dir_parts)):
+ if object_dir_parts[i] == "vegetation":
+ veg_root = os.sep.join(object_dir_parts[: i + 1])
+ break
+ if veg_root:
+ textures_dir = os.path.join(veg_root, "materials", "textures")
+
+ # If no textures directory found, return segments unchanged
+ if not os.path.exists(textures_dir):
+ return segments
+
+ # Categorize object by name
+ object_name_lower = object_name.lower()
+ object_categories = []
+
+ # Common categories
+ category_keywords = {
+ "tree": [
+ "tree",
+ "pine",
+ "oak",
+ "maple",
+ "birch",
+ "cedar",
+ "ash",
+ "spruce",
+ "poplar",
+ "aspen",
+ "beech",
+ "dogwood",
+ "cypress",
+ "hemlock",
+ ],
+ "palm": ["palm", "frond"],
+ "flower": ["flower", "bloom", "blossom", "rose", "tulip", "lily"],
+ "grass": [
+ "grass",
+ "reed",
+ "sedge",
+ "fern",
+ "bamboo",
+ "pampas",
+ "fountain",
+ "switchgrass",
+ ],
+ "bush": [
+ "bush",
+ "shrub",
+ "boxwood",
+ "barberry",
+ "lilac",
+ "lupin",
+ "daphne",
+ "forsythia",
+ "vibernum",
+ "rhododendron",
+ ],
+ }
+
+ # Determine categories
+ for category, keywords in category_keywords.items():
+ if any(keyword in object_name_lower for keyword in keywords):
+ object_categories.append(category)
+
+ if not object_categories:
+ # Default to tree if no other category matched
+ object_categories = ["tree"]
+
+ # Define generic texture sets for each category and part
+ generic_textures = {
+ "tree": {
+ "bark": {
+ "diffuse": "pinebark1_basecolor.png",
+ "normal": "pinebark1_normal.png",
+ "roughness": "pinebark1_roughness.png",
+ },
+ "leaf": {
+ "diffuse": "oakleaves1_basecolor.png",
+ "normal": "oakleaves1_normal.png",
+ "roughness": "oakleaves1_roughness.png",
+ },
+ },
+ "palm": {
+ "bark": {
+ "diffuse": "bark1_basecolor.png",
+ "normal": "bark1_normal.png",
+ "roughness": "bark1_roughness.png",
+ },
+ "leaf": {
+ "diffuse": "palmleaves_mat_basecolor.png",
+ "normal": "palmleaves_mat_normal.png",
+ "roughness": "palmleaves_mat_roughness.png",
+ },
+ },
+ "flower": {
+ "stem": {
+ "diffuse": "bark2_basecolor.png",
+ "normal": "bark2_normal.png",
+ "roughness": "bark2_roughness.png",
+ },
+ "petal": {
+ "diffuse": "goldenchain_flowers_basecolor.png",
+ "normal": "goldenchain_flowers_normal.png",
+ "roughness": "goldenchain_flowers_roughness.png",
+ },
+ },
+ "grass": {
+ "blade": {
+ "diffuse": "ashleaves1_basecolor.png",
+ "normal": "ashleaves1_normal.png",
+ "roughness": "ashleaves1_roughness.png",
+ }
+ },
+ "bush": {
+ "branch": {
+ "diffuse": "bark3_basecolor.png",
+ "normal": "bark3_normal.png",
+ "roughness": "bark3_roughness.png",
+ },
+ "leaf": {
+ "diffuse": "dogwood_leaf_basecolor.png",
+ "normal": "dogwood_leaf_normal.png",
+ "roughness": "dogwood_leaf_roughness.png",
+ },
+ },
+ }
+
+ # Special material name to texture mappings for problematic cases
+ special_material_textures = {
+ # Special material names
+ "Lupin_m": {
+ "diffuse": "lupin_basecolor.png",
+ "normal": "lupin_normal.png",
+ "roughness": "lupin_roughness.png",
+ },
+ "Dagger_M": {
+ "diffuse": "plantatlas1_basecolor.png",
+ "normal": "plantatlas1_normal.png",
+ "roughness": "plantatlas1_roughness.png",
+ },
+ "bark3": {
+ "diffuse": "bark3_basecolor.png",
+ "normal": "bark3_normal.png",
+ "roughness": "bark3_roughness.png",
+ },
+ "Pampas_flower": {
+ "diffuse": "pampas_flower.png",
+ "normal": "fanpalm_normal.png", # Fallback normal map
+ "roughness": "fanpalm_roughness.png", # Fallback roughness map
+ },
+ "FountainGrass": {
+ "diffuse": "fountaingrass_basecolor.png",
+ "normal": "pampas_grass_normal.png",
+ "roughness": "pampas_grass.png",
+ },
+ "TreeBark_01": {
+ "diffuse": "tree_bark_03_diff_2k.png",
+ "normal": "bark1_normal.png",
+ "roughness": "sycamorebark2_roughness.png",
+ },
+ "Barberry": {
+ "diffuse": "barberry_basecolor.png",
+ "normal": "bark3_normal.png", # Fallback
+ "roughness": "bark3_roughness.png", # Fallback
+ },
+ "Century_m": {
+ "diffuse": "century_m_basecolor.png",
+ "normal": "Century_m_Normal.png",
+ "roughness": "Century_m_Roughness.png",
+ },
+ "Rhododendron": {
+ "diffuse": "rhododendron_basecolor.png",
+ "normal": "rhododendron_normal.png",
+ "roughness": "rhododendron_roughness.png",
+ },
+ # Add more problematic materials
+ "Burning_Bush": {
+ "diffuse": "burningbush_leaf_basecolor.png",
+ "normal": "burningbush_leaf_normal.png",
+ "roughness": "burningbush_leaf_roughness.png",
+ },
+ "Cedar_Shrub": {
+ "diffuse": "pinebark1_basecolor.png",
+ "normal": "pinebark1_normal.png",
+ "roughness": "pinebark1_roughness.png",
+ },
+ "Japanese_Flame": {
+ "diffuse": "japaneseflame_basecolor.png",
+ "normal": "japaneseflame_normal.png",
+ "roughness": "japaneseflame_roughness.png",
+ },
+ "Honey_Myrtle": {
+ "diffuse": "honeymyrtle_basecolor.png",
+ "normal": "hollyprivet_normal.png", # Fallback
+ "roughness": "hollyprivet_roughness.png", # Fallback
+ },
+ "Hurricane_Palm_bark_Mat": {
+ "diffuse": "hurricanepalm_bark_basecolor.png",
+ "normal": "hurricanepalm_bark_normal.png",
+ "roughness": "hurricanepalm_bark_roughness.png",
+ },
+ "Australian_Fern_leaves_Mat": {
+ "diffuse": "australianfern_leaves_basecolor.png",
+ "normal": "australianfern_leaves_normal.png",
+ "roughness": "australianfern_leaves_roughness.png",
+ },
+ "Australian_Fern_trunk": {
+ "diffuse": "australianfern_trunk_basecolor.png",
+ "normal": "australianfern_trunk_normal.png",
+ "roughness": "australianfern_trunk_roughness.png",
+ },
+ "Agave_mat": {
+ "diffuse": "agave_basecolor.png",
+ "normal": "agave_normal.png",
+ "roughness": "Agave_Roughness.png",
+ },
+ "Bamboo_leaf_Mat1": {
+ "diffuse": "bambooleaf_basecolor.png",
+ "normal": "bambooleaf_normal.png",
+ "roughness": "bambooleaf_roughness.png",
+ },
+ "Bamboo_shoot_Mat1": {
+ "diffuse": "bambooshoot_basecolor.png",
+ "normal": "bambooshoot_normal.png",
+ "roughness": "bambooshoot_roughness.png",
+ },
+ "CraneLily_mat": {
+ "diffuse": "cranelily_basecolor.png",
+ "normal": "cranelily_normal.png",
+ "roughness": "cranelily_roughness.png",
+ },
+ "CraneLily_mat_2": {
+ "diffuse": "cranelily_basecolor.png",
+ "normal": "cranelily_normal.png",
+ "roughness": "cranelily_roughness.png",
+ },
+ "CraneLily_mat_3": {
+ "diffuse": "cranelily_basecolor.png",
+ "normal": "cranelily_normal.png",
+ "roughness": "cranelily_roughness.png",
+ },
+ "GrassPalm_bark": {
+ "diffuse": "grasspalm_bark_basecolor.png",
+ "normal": "grasspalm_bark_normal.png",
+ "roughness": "grasspalm_bark_roughness.png",
+ },
+ "GrassPalm_leaves": {
+ "diffuse": "grasspalm_leaves_basecolor.png",
+ "normal": "grasspalm_leaves_normal.png",
+ "roughness": "grasspalm_leaves_roughness.png",
+ },
+ }
+
+ # First try to apply special material textures based on material name in each segment
+ for segment_key, segment_info in segments.items():
+ if segment_info is None:
+ continue
+
+ # Skip segments that already have textures
+ if segment_info.get("textures") and len(segment_info["textures"]) > 0:
+ continue
+
+ # Initialize textures dict if needed
+ if "textures" not in segment_info:
+ segment_info["textures"] = {}
+
+ # Get material name
+ material_name = segment_info.get("name", "")
+
+ # Check for special material name mapping
+ if material_name in special_material_textures:
+ for texture_type, texture_file in special_material_textures[
+ material_name
+ ].items():
+ texture_path = os.path.join(textures_dir, texture_file)
+ if os.path.exists(texture_path):
+ segment_info["textures"][texture_type] = texture_path
+
+ # Copy texture if needed
+ if output_textures_dir:
+ copied_path = copy_texture_to_output(
+ texture_path,
+ output_textures_dir,
+ object_name,
+ material_name,
+ texture_type,
+ )
+ if copied_path:
+ segment_info["textures"][
+ f"{texture_type}_copied"
+ ] = copied_path
+
+ # If we found specific textures for this segment, continue to next segment
+ if segment_info.get("textures") and len(segment_info["textures"]) > 0:
+ continue
+
+ # Apply category-based textures if specific ones weren't found
+ material_type = segment_info.get("material_type", "")
+ segment_type = "leaf" # Default
+
+ # Determine segment type
+ if material_type in ["bark", "trunk", "stem", "branch", "stalk"]:
+ segment_type = (
+ "bark" if "bark" in generic_textures[object_categories[0]] else "branch"
+ )
+ elif material_type in ["leaf", "leaves", "foliage", "needle", "frond"]:
+ segment_type = "leaf"
+ elif material_type in ["petal", "flower", "bloom", "blossom"]:
+ segment_type = "petal"
+ elif material_type in ["blade", "grass"]:
+ segment_type = "blade"
+
+ # Get the right texture set based on object category and segment type
+ for category in object_categories:
+ if (
+ category in generic_textures
+ and segment_type in generic_textures[category]
+ ):
+ texture_set = generic_textures[category][segment_type]
+
+ # Apply textures from set
+ for texture_type, texture_file in texture_set.items():
+ texture_path = os.path.join(textures_dir, texture_file)
+ if os.path.exists(texture_path):
+ segment_info["textures"][texture_type] = texture_path
+
+ # Copy texture if needed
+ if output_textures_dir:
+ copied_path = copy_texture_to_output(
+ texture_path,
+ output_textures_dir,
+ object_name,
+ material_name or segment_key,
+ texture_type,
+ )
+ if copied_path:
+ segment_info["textures"][
+ f"{texture_type}_copied"
+ ] = copied_path
+
+ # Break once we found a suitable texture set
+ if segment_info.get("textures") and len(segment_info["textures"]) > 0:
+ break
+
+ # If we still don't have textures, try to find them by looking for any textures that might match
+ if not segment_info.get("textures") or len(segment_info["textures"]) == 0:
+ # Try to find any textures that might match by name
+ object_dir_lower = object_dir.lower()
+ material_name_lower = material_name.lower() if material_name else ""
+ segment_key_lower = segment_key.lower()
+ object_name_lower = object_name.lower()
+
+ # Look in the textures directory for matching textures
+ for texture_file in os.listdir(textures_dir):
+ texture_lower = texture_file.lower()
+
+ # Try to find matches by object name, material name, or segment key
+ if (
+ object_name_lower in texture_lower
+ or material_name_lower in texture_lower
+ or segment_key_lower in texture_lower
+ ):
+
+ # Determine texture type
+ texture_type = None
+ if "basecolor" in texture_lower or "diffuse" in texture_lower:
+ texture_type = "diffuse"
+ elif "normal" in texture_lower:
+ texture_type = "normal"
+ elif "roughness" in texture_lower:
+ texture_type = "roughness"
+
+ if texture_type:
+ texture_path = os.path.join(textures_dir, texture_file)
+ segment_info["textures"][texture_type] = texture_path
+
+ # Copy texture if needed
+ if output_textures_dir:
+ copied_path = copy_texture_to_output(
+ texture_path,
+ output_textures_dir,
+ object_name,
+ material_name or segment_key,
+ texture_type,
+ )
+ if copied_path:
+ segment_info["textures"][
+ f"{texture_type}_copied"
+ ] = copied_path
+
+ # If still missing textures, apply default textures
+ for segment_key, segment_info in segments.items():
+ if segment_info is None:
+ continue
+
+ if not segment_info.get("textures"):
+ segment_info["textures"] = {}
+
+ # Check if we're missing any texture types
+ missing_types = []
+ for texture_type in ["diffuse", "normal", "roughness"]:
+ if texture_type not in segment_info["textures"]:
+ missing_types.append(texture_type)
+
+ if not missing_types:
+ continue
+
+ # Determine segment type again
+ material_type = segment_info.get("material_type", "")
+ segment_type = "leaf" # Default
+
+ if material_type in ["bark", "trunk", "stem", "branch", "stalk"]:
+ segment_type = (
+ "bark" if "bark" in generic_textures[object_categories[0]] else "branch"
+ )
+ elif material_type in ["leaf", "leaves", "foliage", "needle", "frond"]:
+ segment_type = "leaf"
+ elif material_type in ["petal", "flower", "bloom", "blossom"]:
+ segment_type = "petal"
+ elif material_type in ["blade", "grass"]:
+ segment_type = "blade"
+
+ # Apply default textures from the first applicable category
+ for category in object_categories:
+ if (
+ category in generic_textures
+ and segment_type in generic_textures[category]
+ ):
+ for texture_type in missing_types:
+ if texture_type in generic_textures[category][segment_type]:
+ texture_file = generic_textures[category][segment_type][
+ texture_type
+ ]
+ texture_path = os.path.join(textures_dir, texture_file)
+
+ if os.path.exists(texture_path):
+ segment_info["textures"][texture_type] = texture_path
+
+ # Copy texture if needed
+ if output_textures_dir:
+ copied_path = copy_texture_to_output(
+ texture_path,
+ output_textures_dir,
+ object_name,
+ segment_info.get("name", segment_key),
+ texture_type,
+ )
+ if copied_path:
+ segment_info["textures"][
+ f"{texture_type}_copied"
+ ] = copied_path
+
+ # Break once we've applied textures from a category
+ if all(
+ texture_type in segment_info["textures"]
+ for texture_type in missing_types
+ ):
+ break
+
+ return segments
+
+
+def extract_materials_from_usd(
+ usd_file_path, dataset_type=None, output_textures_dir=None
+):
+ """
+ Extract material information from a USD file with improved handling of material bindings.
+
+ Args:
+ usd_file_path: Path to the USD file
+ dataset_type: Type of dataset (residential, commercial, etc.)
+
+ Returns:
+ Dictionary with material information
+ """
+ logging.info(f"Extracting materials from {usd_file_path}")
+ result = {
+ "object_name": os.path.splitext(os.path.basename(usd_file_path))[0],
+ "dataset_type": dataset_type,
+ "file_path": usd_file_path,
+ "date_processed": datetime.datetime.now().isoformat(),
+ "segments": {},
+ }
+
+ # Open the USD stage
+ try:
+ stage = Usd.Stage.Open(usd_file_path)
+ if not stage:
+ logging.error(f"Could not open USD file: {usd_file_path}")
+ return None
+ except Exception as e:
+ logging.error(f"Error opening USD file {usd_file_path}: {str(e)}")
+ return None
+
+ # Track all materials we find in the stage
+ all_materials = {}
+
+ # First pass: collect all materials and their properties
+ logging.info("First pass: collecting all materials")
+ for prim in stage.Traverse():
+ if prim.IsA(UsdShade.Material):
+ material = UsdShade.Material(prim)
+ material_path = str(prim.GetPath())
+ material_name = prim.GetName()
+
+ # Store material info with default values
+ all_materials[material_path] = {
+ "name": material_name,
+ "material_type": material_name, # Default to name
+ "textures": {},
+ }
+
+ # Process material's shaders to find textures
+ # Correctly get all the shader prims in this material
+ shader_prims = []
+ for child_prim in Usd.PrimRange(prim):
+ if child_prim.IsA(UsdShade.Shader):
+ shader_prims.append(child_prim)
+
+ for shader_prim in shader_prims:
+ shader = UsdShade.Shader(shader_prim)
+ if not shader:
+ continue
+
+ # Inspect shader inputs for textures
+ for input in shader.GetInputs():
+ input_name = input.GetBaseName()
+
+ # Check if this input has a connected source that's an asset
+ if input.HasConnectedSource():
+ source = input.GetConnectedSource()
+ if source:
+ source_shader, source_output, _ = source
+ source_prim = source_shader.GetPrim()
+
+ # Check if the source is a texture
+ if source_prim.IsA(UsdShade.Shader):
+ source_shader_id = UsdShade.Shader(
+ source_prim
+ ).GetShaderId()
+ if "texture" in str(source_shader_id).lower():
+ # Try to find the file asset path
+ for source_input in UsdShade.Shader(
+ source_prim
+ ).GetInputs():
+ if source_input.GetBaseName() in [
+ "file",
+ "filename",
+ "filePath",
+ "varname",
+ ]:
+ asset_path = source_input.Get()
+ if asset_path:
+ # Determine texture type from connection patterns
+ tex_type = "unknown"
+ if (
+ "diffuse" in input_name.lower()
+ or "albedo" in input_name.lower()
+ or "color" in input_name.lower()
+ ):
+ tex_type = "diffuse"
+ elif "normal" in input_name.lower():
+ tex_type = "normal"
+ elif "roughness" in input_name.lower():
+ tex_type = "roughness"
+ elif "metallic" in input_name.lower():
+ tex_type = "metallic"
+ elif "specular" in input_name.lower():
+ tex_type = "specular"
+ elif (
+ "displacement" in input_name.lower()
+ ):
+ tex_type = "displacement"
+
+ # Store texture path
+ logging.info(
+ f"Found texture: {tex_type} = {asset_path} for material {material_name}"
+ )
+ all_materials[material_path][
+ "textures"
+ ][tex_type] = str(asset_path)
+
+ # Direct asset inputs (not connected through other shaders)
+ elif input.GetTypeName() == "asset":
+ asset_path = input.Get()
+ if asset_path:
+ # Determine texture type from input name
+ tex_type = "unknown"
+ if (
+ "diffuse" in input_name.lower()
+ or "albedo" in input_name.lower()
+ or "color" in input_name.lower()
+ ):
+ tex_type = "diffuse"
+ elif "normal" in input_name.lower():
+ tex_type = "normal"
+ elif "roughness" in input_name.lower():
+ tex_type = "roughness"
+ elif "metallic" in input_name.lower():
+ tex_type = "metallic"
+ elif "specular" in input_name.lower():
+ tex_type = "specular"
+ elif "displacement" in input_name.lower():
+ tex_type = "displacement"
+
+ # Store texture path
+ logging.info(
+ f"Found direct texture: {tex_type} = {asset_path} for material {material_name}"
+ )
+ all_materials[material_path]["textures"][tex_type] = str(
+ asset_path
+ )
+
+ # Second pass: find all material bindings
+ logging.info("Second pass: finding material bindings")
+
+ # Process meshes and their subsets
+ for prim in stage.Traverse():
+ if prim.IsA(UsdGeom.Mesh):
+ mesh = UsdGeom.Mesh(prim)
+ mesh_name = prim.GetName()
+ logging.info(f"Processing mesh: {mesh_name}")
+
+ # First check direct binding on the mesh
+ binding_api = UsdShade.MaterialBindingAPI(prim)
+ direct_binding = binding_api.GetDirectBinding()
+ direct_material = None
+
+ if direct_binding.GetMaterial():
+ direct_material = direct_binding.GetMaterial()
+ material_path = str(direct_material.GetPath())
+ logging.info(f" Found direct material binding: {material_path}")
+
+ if material_path in all_materials:
+ # Create segment for the whole mesh
+ segment_key = f"{mesh_name}_whole"
+ material_info = all_materials[material_path].copy()
+ material_info["semantic_usage"] = mesh_name
+
+ result["segments"][segment_key] = material_info
+ logging.info(
+ f" Created segment {segment_key} with material {material_path}"
+ )
+
+ # Then check GeomSubsets - these are more specific material assignments
+ imageable = UsdGeom.Imageable(prim)
+ subsets = UsdGeom.Subset.GetGeomSubsets(imageable)
+
+ if subsets:
+ logging.info(f" Found {len(subsets)} geom subsets for {mesh_name}")
+ for subset in subsets:
+ subset_prim = subset.GetPrim()
+ subset_name = subset_prim.GetName()
+ family = (
+ subset.GetFamilyNameAttr().Get()
+ if subset.GetFamilyNameAttr()
+ else "unknown"
+ )
+
+ logging.info(
+ f" Processing subset: {subset_name} (Family: {family})"
+ )
+
+ # Check material binding on subset
+ subset_binding_api = UsdShade.MaterialBindingAPI(subset_prim)
+ subset_direct_binding = subset_binding_api.GetDirectBinding()
+
+ if subset_direct_binding.GetMaterial():
+ subset_material = subset_direct_binding.GetMaterial()
+ subset_material_path = str(subset_material.GetPath())
+ logging.info(
+ f" Found subset material binding: {subset_material_path}"
+ )
+
+ if subset_material_path in all_materials:
+ # Create segment for this subset
+ segment_key = subset_name
+ material_info = all_materials[subset_material_path].copy()
+ material_info["semantic_usage"] = subset_name
+
+ result["segments"][segment_key] = material_info
+ logging.info(
+ f" Created segment {segment_key} with material {subset_material_path}"
+ )
+
+ # If no subsets but we have a direct material, use that
+ if not subsets and direct_material:
+ material_path = str(direct_material.GetPath())
+
+ if material_path in all_materials:
+ # Create segment for the whole mesh
+ segment_key = mesh_name
+ material_info = all_materials[material_path].copy()
+ material_info["semantic_usage"] = mesh_name
+
+ result["segments"][segment_key] = material_info
+ logging.info(
+ f" No subsets, created segment {segment_key} with material {material_path}"
+ )
+
+ # Final check - make sure we have segments
+ if not result["segments"]:
+ logging.warning(f"No material segments found in {usd_file_path}")
+
+ # Last resort - add all materials as segments
+ for material_path, material_info in all_materials.items():
+ material_name = material_info["name"]
+ segment_key = f"material_{material_name}"
+
+ result["segments"][segment_key] = material_info.copy()
+ result["segments"][segment_key]["semantic_usage"] = material_name
+
+ logging.info(
+ f"Added material {material_name} as segment {segment_key} (last resort)"
+ )
+
+ logging.info(
+ f"Extracted {len(result['segments'])} material segments from {usd_file_path}"
+ )
+ return result
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/residential.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/residential.py
new file mode 100644
index 0000000000000000000000000000000000000000..92a132a38c6ab7ba1a2fcf5b5e2b84c5211e0cae
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/residential.py
@@ -0,0 +1,582 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ RESIDENTIAL_BASE_DIR,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
+ render_sphere_with_texture,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
+ analyze_material_with_vlm,
+ parse_vlm_properties,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ extract_materials_from_usd,
+)
+import re
+from tqdm import tqdm
+import os
+import logging
+import copy
+
+PROMPTS = {
+ "few_shot_examples": (
+ """
+Example 1:
+Material: metal
+Usage: structural component
+Object name: SteelBeam
+
+Analysis:
+Step 1: Based on the images, this appears to be a standard structural steel with a matte gray finish.
+Step 2: The surface has medium roughness with some subtle texture visible in the reflection pattern.
+Step 3: Considering its usage as a structural component, this is likely a carbon steel alloy.
+Step 4: Comparing with reference materials, standard structural steel typically has:
+ - High stiffness (Young's modulus ~200 GPa)
+ - Medium Poisson's ratio typical of metals
+ - High density consistent with iron-based alloys
+
+Young's modulus: 2.0e11 Pa
+Poisson's ratio: 0.29
+Density: 7800 kg/m^3
+
+Example 2:
+Material: plastic
+Usage: household container
+Object name: PlasticContainer
+
+Analysis:
+Step 1: The material shows the characteristic smooth, uniform appearance of a consumer plastic.
+Step 2: It has moderate gloss with some translucency and a slight texture.
+Step 3: Given its household container application, this is likely polypropylene.
+Step 4: The visual and contextual evidence suggests:
+ - Medium-low stiffness typical of polyolefin plastics
+ - Higher Poisson's ratio indicating good lateral deformation
+ - Low-medium density typical of consumer thermoplastics
+
+Young's modulus: 1.3e9 Pa
+Poisson's ratio: 0.42
+Density: 950 kg/m^3
+
+Example 3:
+Material: fabric
+Usage: furniture covering
+Object name: FabricCouch
+
+Analysis:
+Step 1: The material shows a woven textile structure with visible fibers.
+Step 2: The surface has significant texture with a matte appearance and no specular highlights.
+Step 3: As furniture upholstery, this is likely a synthetic or natural fiber blend.
+Step 4: Based on the visual characteristics and usage:
+ - Low stiffness as expected for flexible textiles
+ - Medium-high Poisson's ratio from the woven structure
+ - Low density typical of fibrous materials
+
+Young's modulus: 1.2e8 Pa
+Poisson's ratio: 0.38
+Density: 300 kg/m^3
+
+Example 4:
+Material: organic
+Usage: decorative element
+Object name: DriedLeaf
+
+Analysis:
+Step 1: This is an organic material with the characteristic shape and structure of a dried leaf.
+Step 2: The surface shows visible veins, a matte finish, and a brittle, thin structure.
+Step 3: As a dried leaf, it's a natural cellulose-based composite material.
+Step 4: Considering similar organic materials like paper and dried plant fibers:
+ - Low-medium stiffness in the fiber direction
+ - Medium Poisson's ratio reflecting the fibrous structure
+ - Low density typical of dried plant matter
+
+Young's modulus: 2.5e9 Pa
+Poisson's ratio: 0.30
+Density: 400 kg/m^3
+"""
+ ),
+ "query_prompt": (
+ """
+Based on the provided images and context information, analyze the material properties.
+Note: The material segment might be internal to the object and not visible from the outside.
+
+Respond using EXACTLY the following format (do not deviate from this structure):
+
+Analysis:
+Step 1: Identify the material class/type based on visual appearance
+Step 2: Describe the surface characteristics (texture, reflectivity, color)
+Step 3: Determine the specific material subtype considering its usage
+Step 4: Reason through each property estimate based on visual and contextual clues
+
+Young's modulus: Pa
+Poisson's ratio:
+Density: kg/m^3
+
+Critical Instructions:
+1. You MUST provide numerical estimates for ALL materials, including organic or unusual materials
+2. For natural materials like leaves, wood, or paper, provide estimates based on similar materials with known properties
+3. Never use "N/A", "unknown", or any non-numeric responses for the material properties
+4. For Poisson's ratio, provide a simple decimal number (like 0.3 or 0.42)
+5. Each property should be on its own line with exact formatting shown above
+"""
+ ),
+}
+
+
+def make_user_prompt(
+ material_type, semantic_usage, object_name, has_texture_sphere=True
+):
+ intro_text = (
+ """
+You are a materials science expert analyzing two images:
+1. A photo of the full object (showing how the material appears in context).
+2. A sphere with the material's texture (showing color/roughness/reflectivity in isolation).
+
+Using both images and the information below, identify the real-world material and estimate its mechanical properties.
+"""
+ if has_texture_sphere
+ else """
+You are a materials science expert analyzing an image of the full object (showing how the material appears in context).
+
+Using this image and the information below, identify the real-world material and estimate its mechanical properties.
+"""
+ )
+
+ return f"""{intro_text}
+Material context:
+ * Material type: {material_type}
+ * Usage: {semantic_usage}
+ * Object name: {object_name}
+
+Your task is to provide three specific properties:
+1. Young's modulus (in Pa using scientific notation)
+2. Poisson's ratio (a value between 0.0 and 0.5)
+3. Density (in kg/m^3 using scientific notation)
+"""
+
+
+# Use the centralized parser function from vlm.py instead
+parse_vlm_output = parse_vlm_properties
+
+
+def list_residential_objects():
+ """
+ List all available residential objects in the residential directory.
+ """
+ usd_files = []
+ print("\nAvailable residential objects:")
+ for root, _, files in os.walk(RESIDENTIAL_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_files.append(os.path.join(root, file))
+ print(f" - {os.path.basename(root)}/{file}")
+ print()
+
+
+def process_residential(
+ vlm_model,
+ vlm_processor,
+ limit=None,
+ processed_objects=None,
+ output_file=None,
+ existing_results=None,
+):
+ usd_files = []
+ for root, _, files in os.walk(RESIDENTIAL_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_files.append(os.path.join(root, file))
+
+ logging.info(f"Found {len(usd_files)} USD files in residential dataset")
+
+ # Initialize tracking sets and results
+ processed_objects = set() if processed_objects is None else processed_objects
+ existing_results = [] if existing_results is None else existing_results
+
+ # Build a set of already processed object names from existing_results
+ existing_object_names = {
+ result.get("object_name")
+ for result in existing_results
+ if "object_name" in result
+ }
+ logging.info(
+ f"Found {len(existing_object_names)} already processed objects in existing results"
+ )
+
+ # Add names from existing_results to processed_objects to avoid reprocessing
+ processed_objects.update(existing_object_names)
+
+ # Create a copy of existing_results to avoid modifying the original
+ all_results = copy.deepcopy(existing_results)
+
+ usd_files.sort()
+
+ if limit and limit > 0:
+ usd_files = usd_files[:limit]
+
+ success_count = 0
+ failed_objects = []
+ total_segments = 0
+ unique_materials = set()
+ materials_per_object = {}
+ total_rendered_segments = 0
+ total_vlm_segments = 0
+
+ # Count total segments from existing results
+ for result in existing_results:
+ total_segments += len(result.get("segments", {}))
+
+ # Statistics for texture availability
+ segments_with_texture = 0
+ segments_without_texture = 0
+ segments_with_thumbnail_only = 0
+
+ # Track processed files to avoid duplicates from the same directory
+ processed_files = set()
+
+ for usd_file in tqdm(usd_files, desc=f"Processing residential dataset"):
+ # Extract object name from path
+ object_name = os.path.basename(os.path.dirname(usd_file))
+
+ # Skip if we already processed this exact file
+ if usd_file in processed_files:
+ continue
+
+ # Skip objects that have already been processed
+ if object_name in processed_objects:
+ logging.info(f"Skipping already processed object: {object_name}")
+ continue
+
+ try:
+ directory = os.path.dirname(usd_file)
+
+ # Extract material information
+ result = extract_materials_from_usd(usd_file, "residential")
+
+ if result:
+ # Add to processed_files to avoid duplicates
+ processed_files.add(usd_file)
+
+ # Track statistics
+ segments = result.get("segments", {})
+ total_segments += len(segments)
+
+ # Remove object_name and note fields from segments
+ for segment_key, segment_info in segments.items():
+ if "object_name" in segment_info:
+ del segment_info["object_name"]
+ if "note" in segment_info:
+ del segment_info["note"]
+
+ # Count unique materials for this object
+ object_materials = set()
+ for segment_name, segment_info in segments.items():
+ material_name = segment_info.get("material_type", "unknown")
+ unique_materials.add(material_name)
+ object_materials.add(material_name)
+
+ # Record materials per object
+ if len(segments) > 0:
+ materials_per_object[object_name] = len(object_materials)
+
+ # Get thumbnail path if available
+ thumb_path = None
+ # For residential dataset, thumbnails are in .thumbs/256x256 directory
+ thumb_dir = os.path.join(
+ os.path.dirname(usd_file), ".thumbs", "256x256"
+ )
+
+ has_thumbnail = False
+ if os.path.exists(thumb_dir):
+ # Try to find a thumbnail matching the USD filename
+ usd_filename = os.path.basename(usd_file)
+ thumb_candidates = [
+ # Regular thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.png"),
+ # Auto-generated thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.auto.png"),
+ ]
+
+ for candidate in thumb_candidates:
+ if os.path.exists(candidate):
+ thumb_path = candidate
+ has_thumbnail = True
+ logging.info(f"Found thumbnail: {thumb_path}")
+ break
+
+ # Process VLM for all segments if VLM model is provided
+ os.makedirs("/tmp/vlm", exist_ok=True)
+
+ if vlm_model and len(segments) > 0:
+ for segment_key, segment_info in segments.items():
+ textures = segment_info.get("textures", {})
+
+ # Log texture information for diagnostics
+ logging.info(
+ f"Segment {segment_key} has textures: {list(textures.keys())}"
+ )
+
+ # Check if we have either a normal or roughness texture for rendering
+ has_texture = (
+ "normal" in textures
+ or "roughness" in textures
+ or "diffuse" in textures
+ )
+ if has_texture:
+ # Has texture - render sphere and use with thumbnail
+ segments_with_texture += 1
+ logging.info(
+ f"Rendering texture sphere for {object_name}, segment {segment_key}"
+ )
+
+ # Set up file path for this segment's rendered sphere
+ segment_render_path = f"/tmp/vlm/texture_sphere_{object_name}_{segment_key}.png"
+
+ # Render the textured sphere
+ try:
+ rgb_buffer = render_sphere_with_texture(
+ textures, segment_render_path
+ )
+ logging.info(f"RGB buffer shape: {rgb_buffer.shape}")
+ except Exception as e:
+ logging.error(
+ f"Error rendering texture for {segment_key}: {str(e)}"
+ )
+ segment_render_path = None
+ else:
+ # No texture - just use thumbnail
+ segments_without_texture += 1
+ segment_render_path = None
+ logging.info(
+ f"No texture for {object_name}, segment {segment_key}. Using thumbnail only."
+ )
+
+ # Always try to process with VLM, even if no texture
+ try:
+ # If we have a thumbnail but no texture, still run VLM with just the thumbnail
+ if not has_texture and has_thumbnail:
+ segments_with_thumbnail_only += 1
+ logging.info(
+ f"Using thumbnail only for {object_name}, segment {segment_key}"
+ )
+
+ # Don't run VLM if we have neither texture nor thumbnail
+ if not segment_render_path and not has_thumbnail:
+ logging.warning(
+ f"Skipping VLM for {segment_key} - no texture or thumbnail available"
+ )
+ continue
+
+ # Set semantic usage to segment name but don't store in segment data
+ semantic_usage = segment_key
+ temp_object_name = object_name
+
+ # Create custom prompt based on texture availability
+ custom_prompt = make_user_prompt(
+ segment_info["material_type"],
+ semantic_usage,
+ temp_object_name,
+ has_texture_sphere=segment_render_path is not None,
+ )
+
+ # Store the custom prompt in material_info but not object_name
+ segment_info["user_prompt"] = custom_prompt
+
+ # Debug: Log the prompt type based on texture availability
+ if segment_render_path is not None:
+ logging.info(
+ f"Using prompt WITH texture sphere for {object_name}, segment {segment_key}"
+ )
+ else:
+ logging.info(
+ f"Using prompt WITHOUT texture sphere for {object_name}, segment {segment_key}"
+ )
+ logging.info(
+ f"PROMPT: {custom_prompt[:100]}..."
+ ) # Print just the beginning of the prompt
+
+ # Create a temporary segment_info with object_name for VLM but don't save to result
+ temp_segment_info = segment_info.copy()
+ temp_segment_info["semantic_usage"] = semantic_usage
+ temp_segment_info["object_name"] = temp_object_name
+
+ vlm_analysis = analyze_material_with_vlm(
+ segment_render_path, # This can be None, in which case only thumbnail is used
+ temp_segment_info, # Use temporary copy with object_name
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name="residential",
+ PROMPTS=PROMPTS,
+ make_user_prompt=make_user_prompt,
+ parse_vlm_output=parse_vlm_output,
+ )
+
+ # Add VLM analysis to segment info
+ if vlm_analysis and "error" not in vlm_analysis:
+ segment_info["vlm_analysis"] = vlm_analysis.get(
+ "vlm_analysis"
+ )
+
+ if vlm_analysis.get("youngs_modulus") is not None:
+ segment_info["youngs_modulus"] = vlm_analysis.get(
+ "youngs_modulus"
+ )
+
+ if vlm_analysis.get("poissons_ratio") is not None:
+ segment_info["poissons_ratio"] = vlm_analysis.get(
+ "poissons_ratio"
+ )
+
+ if vlm_analysis.get("density") is not None:
+ segment_info["density"] = vlm_analysis.get(
+ "density"
+ )
+
+ total_vlm_segments += 1
+ logging.info(
+ f"VLM analysis successful for {segment_key}:"
+ )
+ logging.info(
+ f" Young's modulus: {vlm_analysis.get('youngs_modulus')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {vlm_analysis.get('poissons_ratio')}"
+ )
+ logging.info(
+ f" Density: {vlm_analysis.get('density')}"
+ )
+ else:
+ logging.error(
+ f"VLM analysis failed for {segment_key}: {vlm_analysis.get('error', 'Unknown error')}"
+ )
+ except Exception as e:
+ import traceback
+
+ logging.error(
+ f"Error during VLM analysis for {segment_key}: {str(e)}"
+ )
+ logging.error(traceback.format_exc())
+
+ total_rendered_segments += 1
+
+ all_results.append(result) # Add to our local copy of results
+ processed_objects.add(object_name) # Mark as processed
+
+ # Incremental save after each object if output file is provided
+ if output_file:
+ try:
+ with open(output_file, "w") as f:
+ import json
+ from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ UsdJsonEncoder,
+ )
+
+ # Debug save contents
+ logging.info(
+ f"Saving checkpoint with {len(all_results)} objects"
+ )
+
+ # Ensure result types are JSON serializable
+ for idx, item in enumerate(all_results):
+ if "segments" in item:
+ for seg_key, seg_info in item["segments"].items():
+ # Remove object_name and note fields if they exist
+ if "object_name" in seg_info:
+ del seg_info["object_name"]
+ if "note" in seg_info:
+ del seg_info["note"]
+
+ if "textures" in seg_info and isinstance(
+ seg_info["textures"], dict
+ ):
+ # Convert any non-serializable texture paths to strings
+ serializable_textures = {}
+ for tex_type, tex_path in seg_info[
+ "textures"
+ ].items():
+ serializable_textures[tex_type] = str(
+ tex_path
+ )
+ seg_info["textures"] = serializable_textures
+
+ # Try to serialize to a string first to check for issues
+ try:
+ json_str = json.dumps(
+ all_results, cls=UsdJsonEncoder, indent=4
+ )
+ logging.info(
+ f"JSON serialization successful, string length: {len(json_str)}"
+ )
+
+ # Now write to file
+ f.write(json_str)
+
+ except Exception as json_err:
+ logging.error(
+ f"JSON serialization error: {str(json_err)}"
+ )
+ # Try to identify problematic objects
+ for i, item in enumerate(all_results):
+ try:
+ json.dumps(item, cls=UsdJsonEncoder)
+ except Exception as e:
+ logging.error(
+ f"Error serializing object {i}: {str(e)}"
+ )
+ raise json_err # Re-raise to be caught by outer exception handler
+
+ except Exception as e:
+ logging.error(f"Error saving checkpoint: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+
+ success_count += 1
+ else:
+ logging.warning(f"No material information extracted for {usd_file}")
+ failed_objects.append(object_name)
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error processing {usd_file}: {str(e)}")
+ logging.error(traceback.format_exc())
+ failed_objects.append(os.path.basename(os.path.dirname(usd_file)))
+
+ # Convert materials_per_object to list format for consistency with simready
+ materials_per_object_list = []
+ for obj_name, count in materials_per_object.items():
+ materials_per_object_list.append(obj_name)
+
+ # Log texture statistics
+ logging.info("Texture Statistics:")
+ logging.info(f" Total segments processed: {total_segments}")
+ logging.info(f" Segments with textures: {segments_with_texture}")
+ logging.info(f" Segments without textures: {segments_without_texture}")
+ logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
+ logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
+
+ return (
+ all_results,
+ len(usd_files),
+ success_count,
+ failed_objects,
+ total_segments,
+ total_rendered_segments,
+ total_vlm_segments,
+ list(unique_materials),
+ materials_per_object_list,
+ )
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/simready.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/simready.py
new file mode 100644
index 0000000000000000000000000000000000000000..02ec6e25b5624f70906ad0e5d70affd740b90661
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/simready.py
@@ -0,0 +1,1540 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import glob
+
+from pxr import Usd, UsdShade
+import logging
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ round_float_to_2dp,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ SIMREADY_ASSET_CLASS_MAPPING as ASSET_CLASS_MAPPING,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ SIMREADY_MATERIALS_DIR as PHYSICS_MATERIALS_DIR,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ SIMREADY_PROPS_DIR as PROPS_DIR,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
+ render_sphere_with_texture,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
+ analyze_material_with_vlm,
+ parse_vlm_properties,
+)
+import re
+from tqdm import tqdm
+
+PROMPTS = {
+ "few_shot_examples": (
+ """
+Example 1:
+Material: metal
+Opacity: opaque
+Density: 7800 kg/m^3
+Dynamic friction: 0.3
+Static friction: 0.4
+Restitution: 0.3
+Usage: structural component
+
+Analysis:
+Step 1: Based on the images, this appears to be a standard structural steel with a matte gray finish.
+Step 2: The surface has medium roughness with some subtle texture visible in the reflection pattern.
+Step 3: The physical properties (density, friction values, restitution) are consistent with carbon steel.
+Step 4: Considering the usage and measured properties:
+ - High stiffness (Young's modulus ~200 GPa) based on typical steel values
+ - Medium Poisson's ratio typical of metals
+ - High density matching the measured 7800 kg/m^3
+
+Young's modulus: 2.0e11 Pa
+Poisson's ratio: 0.29
+Density: 7800 kg/m^3
+
+Example 2:
+Material: plastic
+Opacity: opaque
+Density: 950 kg/m^3
+Dynamic friction: 0.25
+Static friction: 0.35
+Restitution: 0.6
+Usage: household container
+
+Analysis:
+Step 1: The material shows the characteristic smooth, uniform appearance of a consumer plastic.
+Step 2: It has moderate gloss with some translucency and a slight texture.
+Step 3: The physical properties (medium-low density, moderate friction, higher restitution) match polypropylene.
+Step 4: Based on these observations and measurements:
+ - Medium-low stiffness typical of polyolefin plastics
+ - Higher Poisson's ratio indicating good lateral deformation
+ - Density matching the measured 950 kg/m^3
+
+Young's modulus: 1.3e9 Pa
+Poisson's ratio: 0.42
+Density: 950 kg/m^3
+
+Example 3:
+Material: fabric
+Opacity: opaque
+Density: 300 kg/m^3
+Dynamic friction: 0.55
+Static friction: 0.75
+Restitution: 0.2
+Usage: furniture covering
+
+Analysis:
+Step 1: The material shows a woven textile structure with visible fibers.
+Step 2: The surface has significant texture with a matte appearance and no specular highlights.
+Step 3: The physical properties (low density, high friction, low restitution) match a woven textile.
+Step 4: Based on these observations and measurements:
+ - Low stiffness as expected for flexible textiles
+ - Medium-high Poisson's ratio from the woven structure
+ - Density matching the measured 300 kg/m^3
+
+Young's modulus: 1.2e8 Pa
+Poisson's ratio: 0.38
+Density: 300 kg/m^3
+
+Example 4:
+Material: organic
+Opacity: opaque
+Density: 400 kg/m^3
+Dynamic friction: 0.45
+Static friction: 0.65
+Restitution: 0.15
+Usage: decorative element
+
+Analysis:
+Step 1: This is an organic material with the characteristic structure of natural fibers.
+Step 2: The surface shows a natural pattern, matte finish, and relatively brittle structure.
+Step 3: The physical properties (low density, moderate-high friction, low restitution) align with plant-based materials.
+Step 4: Considering similar organic materials and the measured properties:
+ - Low-medium stiffness in the fiber direction
+ - Medium Poisson's ratio reflecting the fibrous structure
+ - Density matching the measured 400 kg/m^3
+
+Young's modulus: 2.5e9 Pa
+Poisson's ratio: 0.30
+Density: 400 kg/m^3
+"""
+ ),
+ "query_prompt": (
+ """
+Based on the provided images and context information, analyze the material properties.
+Note: The material segment might be internal to the object and not visible from the outside.
+
+Respond using EXACTLY the following format (do not deviate from this structure):
+
+Analysis:
+Step 1: Identify the material class/type based on visual appearance
+Step 2: Describe the surface characteristics (texture, reflectivity, color)
+Step 3: Determine the specific material subtype considering its physical properties
+Step 4: Reason through each property estimate based on visual and measured data
+
+Young's modulus: Pa
+Poisson's ratio:
+Density: kg/m^3
+
+Critical Instructions:
+1. You MUST provide numerical estimates for ALL materials, including organic or unusual materials
+2. For natural materials like leaves, wood, or paper, provide estimates based on similar materials with known properties
+3. Never use "N/A", "unknown", or any non-numeric responses for the material properties
+4. For Poisson's ratio, provide a simple decimal number (like 0.3 or 0.42)
+5. Each property should be on its own line with exact formatting shown above
+"""
+ ),
+}
+
+
+def make_user_prompt(
+ material_type,
+ opacity,
+ density,
+ dynamic_friction,
+ static_friction,
+ restitution,
+ semantic_usage,
+ has_texture_sphere=True,
+):
+ intro_text = (
+ """
+You are a materials science expert analyzing two images:
+1. A photo of the full object (showing how the material appears in context).
+2. A sphere with the material's texture (showing color/roughness/reflectivity in isolation).
+
+Using both images and the information below, identify the real-world material and estimate its mechanical properties.
+"""
+ if has_texture_sphere
+ else """
+You are a materials science expert analyzing an image of the full object (showing how the material appears in context).
+
+Using this image and the information below, identify the real-world material and estimate its mechanical properties.
+"""
+ )
+
+ return f"""{intro_text}
+Material context:
+ * Material type: {material_type}
+ * Opacity: {opacity}
+ * Density: {density if density is not None else 'unknown'} kg/m^3
+ * Dynamic friction: {dynamic_friction if dynamic_friction is not None else 'unknown'}
+ * Static friction: {static_friction if static_friction is not None else 'unknown'}
+ * Restitution: {restitution if restitution is not None else 'unknown'}
+ * Usage: {semantic_usage}
+
+Your task is to provide three specific properties:
+1. Young's modulus (in Pa using scientific notation)
+2. Poisson's ratio (a value between 0.0 and 0.5)
+3. Density (in kg/m^3 using scientific notation)
+"""
+
+
+# Use the centralized parser function from vlm.py instead
+parse_vlm_output = parse_vlm_properties
+
+
+def list_simready_objects():
+ """
+ List all available props in the SimReady props directory.
+ """
+ if not os.path.isdir(PROPS_DIR):
+ print(f"Error: SimReady props directory not found at {PROPS_DIR}")
+ sys.exit(1)
+
+ print("\nAvailable props:")
+ for prop_dir in sorted(os.listdir(PROPS_DIR)):
+ if os.path.isdir(os.path.join(PROPS_DIR, prop_dir)):
+ print(f" - {prop_dir}")
+ print()
+
+
+def get_usd_file_from_prop_dir(prop_dir):
+ inst_base_files = glob.glob(os.path.join(prop_dir, "*_inst_base.usd"))
+ if inst_base_files:
+ return inst_base_files[0]
+
+ base_files = glob.glob(os.path.join(prop_dir, "*_base.usd"))
+ if base_files:
+ return base_files[0]
+
+ usd_files = glob.glob(os.path.join(prop_dir, "*.usd"))
+ if usd_files:
+ return usd_files[0]
+
+ print(f"Error: No USD file found in {prop_dir}")
+ sys.exit(1)
+
+
+def load_physics_materials():
+ """
+ Load physics material properties from USD files.
+
+ Returns:
+ dict: Dictionary mapping material types to physics properties
+ """
+ physics_materials = {}
+
+ # Check if physics materials directory exists
+ if not os.path.isdir(PHYSICS_MATERIALS_DIR):
+ print(
+ f"Warning: Physics materials directory not found at {PHYSICS_MATERIALS_DIR}"
+ )
+ print("Physics properties will not be available.")
+ return physics_materials
+
+ # Load each physics material file
+ for usda_file in glob.glob(os.path.join(PHYSICS_MATERIALS_DIR, "physics_*.usda")):
+ material_name = (
+ os.path.basename(usda_file).replace("physics_", "").replace(".usda", "")
+ )
+
+ try:
+ stage = Usd.Stage.Open(usda_file)
+ if not stage:
+ print(f"Warning: Could not open physics material file {usda_file}")
+ continue
+
+ # Find material prims with PhysicsMaterialAPI
+ material_prims = []
+ for prim in stage.Traverse():
+ # Check if the prim is a Material and has PhysicsMaterialAPI applied
+ if prim.IsA(UsdShade.Material) or "PhysicsMaterialAPI" in [
+ s.GetName() for s in prim.GetAppliedSchemas()
+ ]:
+ material_prims.append(prim)
+
+ if not material_prims:
+ # Try to find any prim with physics attributes as a fallback
+ for prim in stage.Traverse():
+ if prim.HasAttribute("physics:density") or prim.HasAttribute(
+ "physics:dynamicFriction"
+ ):
+ material_prims.append(prim)
+ break
+
+ if not material_prims:
+ print(f"Warning: No physics material found in {usda_file}")
+ continue
+
+ material_prim = material_prims[0]
+
+ # Extract physics properties
+ density = (
+ material_prim.GetAttribute("physics:density").Get()
+ if material_prim.HasAttribute("physics:density")
+ else None
+ )
+ dynamic_friction = (
+ material_prim.GetAttribute("physics:dynamicFriction").Get()
+ if material_prim.HasAttribute("physics:dynamicFriction")
+ else None
+ )
+ static_friction = (
+ material_prim.GetAttribute("physics:staticFriction").Get()
+ if material_prim.HasAttribute("physics:staticFriction")
+ else None
+ )
+ restitution = (
+ material_prim.GetAttribute("physics:restitution").Get()
+ if material_prim.HasAttribute("physics:restitution")
+ else None
+ )
+
+ # Round values to 2 decimal places
+ physics_materials[material_name] = {
+ "density": round_float_to_2dp(density),
+ "dynamic_friction": round_float_to_2dp(dynamic_friction),
+ "static_friction": round_float_to_2dp(static_friction),
+ "restitution": round_float_to_2dp(restitution),
+ }
+
+ # print(f"Loaded physics material '{material_name}' with properties: density={round_float_to_2dp(density)}, dynamic_friction={round_float_to_2dp(dynamic_friction)}, static_friction={round_float_to_2dp(static_friction)}, restitution={round_float_to_2dp(restitution)}")
+
+ except Exception as e:
+ print(f"Error loading physics material {usda_file}: {str(e)}")
+ import traceback
+
+ traceback.print_exc()
+
+ return physics_materials
+
+
+def parse_material_name(material_name):
+ """
+ Parse a material name according to the SimReady convention.
+ Format: opacity__material-type__specific-usage
+
+ Args:
+ material_name (str): Material name
+
+ Returns:
+ tuple: (opacity, material_type, semantic_usage)
+ """
+ parts = material_name.split("__")
+
+ if len(parts) >= 3:
+ opacity = parts[0]
+ material_type = parts[1]
+ semantic_usage = parts[2]
+ elif len(parts) == 2:
+ opacity = parts[0]
+ material_type = parts[1]
+ semantic_usage = ""
+ else:
+ # Default values if the name doesn't follow the convention
+ opacity = "unknown"
+ material_type = material_name
+ semantic_usage = ""
+
+ return opacity, material_type, semantic_usage
+
+
+def find_textures_for_material(prop_dir, mesh_name, material_type):
+ """
+ Find textures associated with a mesh in the prop directory.
+ This function is used as a fallback when USD-based texture extraction doesn't find textures.
+
+ Args:
+ prop_dir (str): Path to the prop directory
+ mesh_name (str): Name of the mesh segment (can be empty for general search)
+ material_type (str): Type of material (e.g., "metal", "fabric")
+
+ Returns:
+ dict: Dictionary of texture paths by type (albedo, normal, orm)
+ """
+ textures = {}
+
+ # Handle special debug case for problematic objects
+ special_debug = "appleseed_coffeetable" in prop_dir
+
+ if special_debug:
+ logging.info(
+ f"SPECIAL DEBUG TEXTURES: Finding textures for mesh '{mesh_name}', material type '{material_type}' in {prop_dir}"
+ )
+
+ # Check for texture directories, case-insensitive
+ texture_directories = []
+
+ # Directory patterns to check (both lowercase and uppercase first letter)
+ dir_patterns = [
+ os.path.join(prop_dir, "textures"),
+ os.path.join(prop_dir, "Textures"),
+ os.path.join(prop_dir, "materials", "textures"),
+ os.path.join(prop_dir, "materials", "Textures"),
+ os.path.join(prop_dir, "Materials", "textures"),
+ os.path.join(prop_dir, "Materials", "Textures"),
+ ]
+
+ # Check each pattern
+ for dir_path in dir_patterns:
+ if os.path.isdir(dir_path):
+ texture_directories.append(dir_path)
+
+ # Check for material-specific subdirectories
+ for subdir in os.listdir(dir_path):
+ subdir_path = os.path.join(dir_path, subdir)
+ if os.path.isdir(subdir_path) and not subdir.startswith("."):
+ texture_directories.append(subdir_path)
+
+ if special_debug:
+ logging.info(
+ f"SPECIAL DEBUG TEXTURES: Found {len(texture_directories)} texture directories"
+ )
+
+ # Try looking in parent directory if no texture directories found
+ if not texture_directories:
+ parent_dir = os.path.dirname(prop_dir)
+ parent_textures_dir_patterns = [
+ os.path.join(parent_dir, "textures"),
+ os.path.join(parent_dir, "Textures"),
+ ]
+
+ for dir_path in parent_textures_dir_patterns:
+ if os.path.isdir(dir_path):
+ texture_directories.append(dir_path)
+
+ if not texture_directories:
+ logging.warning(f"No textures directory found for {prop_dir}")
+ return {}
+
+ # Extract prop name and mesh parts for texture matching
+ prop_name = os.path.basename(os.path.normpath(prop_dir))
+
+ # Extract the base name without version/variant (e.g., "reflectivetape" from "reflectivetape2m5cm_i01")
+ base_prop_name = prop_name.split("_")[0].lower()
+
+ # Extract material info from mesh name for better targeting
+ mesh_parts = []
+ if mesh_name:
+ # Split by camel case or underscores
+ import re
+
+ mesh_parts = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|[0-9]+", mesh_name)
+ mesh_parts = [part.lower() for part in mesh_parts]
+
+ # Also add common prefixes
+ if mesh_name.startswith("SM_"):
+ mesh_parts.append("sm")
+ if "_" in mesh_name:
+ for part in mesh_name.split("_"):
+ if part.strip():
+ mesh_parts.append(part.lower())
+
+ # Different possible naming patterns
+ patterns = []
+
+ # If mesh name is provided, add specific patterns first
+ if mesh_name:
+ # Direct segment match
+ patterns.append(f"{mesh_name}_*")
+ patterns.append(f"{mesh_name}*_*")
+ # Prop name + segment
+ patterns.append(f"{prop_name}_{mesh_name}_*")
+ patterns.append(f"{prop_name}_{mesh_name}*_*")
+
+ # More generic patterns
+ patterns.extend(
+ [
+ # Material type
+ f"*{material_type}*_*",
+ # T_ prefix patterns
+ f"T_{base_prop_name}*_*",
+ f"T_*{base_prop_name}*_*",
+ f"T_*{material_type}*_*",
+ # Capitalized T_ patterns for case-insensitive matching
+ f"T_{base_prop_name.capitalize()}*_*",
+ f"T_{prop_name.capitalize()}*_*",
+ f"T_{material_type.capitalize()}*_*",
+ # By material type
+ f"*{material_type}*.*",
+ f"{material_type}*.*",
+ # Direct prop name
+ f"{prop_name}*.*",
+ f"*{base_prop_name}*.*",
+ # For materials in subdirectories
+ f"T_*_Albedo.png",
+ f"T_*_Normal.png",
+ f"T_*_ORM.png",
+ f"T_*_Roughness.png",
+ f"T_*_Metallic.png",
+ # Single-letter suffix patterns (e.g., _A.png for Albedo, _N.png for Normal)
+ f"*_A.png", # Albedo
+ f"*_N.png", # Normal
+ f"*_R.png", # Roughness
+ f"*_M.png", # Metallic
+ f"*_O.png", # Occlusion
+ # Simplified match based on mesh name parts
+ f"*{mesh_name.split('_')[-1] if mesh_name and '_' in mesh_name else ''}*.*",
+ ]
+ )
+
+ # Add mesh part-based patterns
+ for part in mesh_parts:
+ if len(part) > 2: # Skip very short parts
+ patterns.append(f"*{part}*.*")
+
+ # For appleseed_coffeetable, add more specific patterns
+ if "appleseed_coffeetable" in prop_dir:
+ patterns.extend(
+ [
+ "Appleseed_CoffeeTable*.*",
+ "*CoffeeTable*.*",
+ "*Coffee*.*",
+ "*Table*.*",
+ "*coffee*.*",
+ "*table*.*",
+ ]
+ )
+
+ # Add material-specific patterns based on mesh name
+ if mesh_name and "Top" in mesh_name:
+ patterns.extend(
+ ["*Wood*.*", "*top*.*", "*Top*.*"] # Wood textures for the top
+ )
+ elif mesh_name and "Leg" in mesh_name:
+ patterns.extend(
+ ["*Metal*.*", "*leg*.*", "*Leg*.*"] # Metal textures for the legs
+ )
+
+ # Search in all texture directories
+ found_textures = []
+ for textures_dir in texture_directories:
+ all_png_files = glob.glob(os.path.join(textures_dir, "*.png"))
+
+ # Skip hidden directories and files
+ all_png_files = [
+ f for f in all_png_files if not os.path.basename(f).startswith(".")
+ ]
+
+ # Log for directories with .png files
+ if all_png_files:
+ logging.info(f"Found {len(all_png_files)} PNG files in {textures_dir}")
+
+ # If there are only a few textures in the directory, they're likely for this prop
+ if len(all_png_files) <= 10:
+ found_textures.extend(all_png_files)
+ else:
+ # Try specific patterns first
+ for pattern in patterns[:-1]: # Skip the last (wildcard) pattern initially
+ matches = glob.glob(os.path.join(textures_dir, pattern))
+ if special_debug and matches:
+ logging.info(
+ f"SPECIAL DEBUG TEXTURES: Pattern '{pattern}' matched {len(matches)} files"
+ )
+ if matches:
+ found_textures.extend(matches)
+
+ # If no matches, try the generic pattern
+ if not found_textures and patterns:
+ found_textures = glob.glob(os.path.join(textures_dir, patterns[-1]))
+ if special_debug and found_textures:
+ logging.info(
+ f"SPECIAL DEBUG TEXTURES: Generic pattern matched {len(found_textures)} files"
+ )
+
+ if special_debug:
+ logging.info(
+ f"SPECIAL DEBUG TEXTURES: Found {len(found_textures)} texture files total"
+ )
+ for texture in found_textures[:5]: # Log up to 5 textures
+ logging.info(f" - {os.path.basename(texture)}")
+ if len(found_textures) > 5:
+ logging.info(f" ... and {len(found_textures) - 5} more")
+
+ # Map texture types, case-insensitive
+ for texture_path in found_textures:
+ texture_name = os.path.basename(texture_path)
+ lower_texture_name = texture_name.lower()
+
+ # Handle BaseColor/Color/Albedo textures
+ if any(
+ term in lower_texture_name for term in ["_basecolor", "_color", "_albedo"]
+ ) or lower_texture_name.endswith("_a.png"):
+ textures["albedo"] = texture_path
+
+ # Handle Normal maps
+ elif any(
+ term in lower_texture_name for term in ["_normal", "_nrm"]
+ ) or lower_texture_name.endswith("_n.png"):
+ textures["normal"] = texture_path
+
+ # Handle Roughness maps
+ elif any(
+ term in lower_texture_name for term in ["_roughness", "_rgh"]
+ ) or lower_texture_name.endswith("_r.png"):
+ textures["roughness"] = texture_path
+
+ # Handle Metallic maps
+ elif any(
+ term in lower_texture_name for term in ["_metallic", "_mtl"]
+ ) or lower_texture_name.endswith("_m.png"):
+ textures["metallic"] = texture_path
+
+ # Handle ORM (combined Occlusion/Roughness/Metallic) maps
+ elif any(term in lower_texture_name for term in ["_orm", "_arm"]):
+ textures["orm"] = texture_path
+
+ # Handle Emissive maps
+ elif any(
+ term in lower_texture_name for term in ["_emissive", "_emission"]
+ ) or lower_texture_name.endswith("_e.png"):
+ textures["emissive"] = texture_path
+
+ # Log what we found
+ if textures:
+ logging.info(
+ f"Found textures for {prop_name}/{mesh_name if mesh_name else 'general'}: {', '.join(textures.keys())}"
+ )
+
+ return textures
+
+
+def extract_materials_from_usd(usd_file_path, prop_name=None, prop_dir=None):
+ """
+ Extract material information from a USD file with improved texture handling.
+ First tries USD-based texture extraction, then falls back to directory searching.
+
+ Args:
+ usd_file_path (str): Path to the USD file
+ prop_name (str, optional): Name of the prop (directory name)
+ prop_dir (str, optional): Path to the prop directory
+
+ Returns:
+ dict: Dictionary mapping segment names to material info
+ """
+ if not os.path.exists(usd_file_path):
+ logging.error(f"USD file not found at {usd_file_path}")
+ return {"object_name": prop_name, "segments": {}}
+
+ try:
+ # Load physics materials
+ physics_materials = load_physics_materials()
+
+ # Open the USD stage
+ stage = Usd.Stage.Open(usd_file_path)
+ if not stage:
+ logging.error(f"Failed to open USD stage from {usd_file_path}")
+ return {"object_name": prop_name, "segments": {}}
+
+ # Get the default prim
+ default_prim = stage.GetDefaultPrim()
+ if not default_prim:
+ default_prim = stage.GetPrimAtPath("/")
+
+ # Create the result dictionary with prop name, dataset type, and category
+ category = ASSET_CLASS_MAPPING.get(prop_name, "unknown")
+ result = {
+ "object_name": prop_name,
+ "category": category,
+ "dataset_type": "simready",
+ "file_path": usd_file_path,
+ "segments": {},
+ }
+
+ # Flag to track if we found any materials
+ found_materials = False
+
+ # Find all mesh prims
+ mesh_prims = []
+ for prim in Usd.PrimRange(default_prim):
+ if prim.GetTypeName() == "Mesh":
+ mesh_prims.append(prim)
+
+ if not mesh_prims:
+ logging.warning(f"No mesh prims found in {usd_file_path}")
+
+ # Process each mesh prim
+ for prim in mesh_prims:
+ mesh_name = prim.GetName()
+
+ # Get the full path for consistent segment key construction
+ full_path = str(prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and default prim if needed
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the common.py convention
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ # Get material binding
+ material_binding = UsdShade.MaterialBindingAPI(prim)
+ bound_material = None
+
+ if material_binding:
+ bound_material = material_binding.GetDirectBinding().GetMaterial()
+ if not bound_material:
+ # Try to get bound material through collections
+ for binding in material_binding.GetCollectionBindings():
+ bound_material = binding.GetMaterial()
+ if bound_material:
+ break
+
+ # If material is found, extract its properties
+ if bound_material:
+ found_materials = True
+
+ # Get material name
+ material_name = bound_material.GetPath().name
+
+ # Parse material name
+ opacity, material_type, semantic_usage = parse_material_name(
+ material_name
+ )
+
+ # Get physics properties for this material type if available
+ physics_props = physics_materials.get(material_type, {})
+
+ # First try USD-based texture extraction
+ textures = {}
+ if prop_dir:
+ # Find the surface shader for this material
+ surface_output = bound_material.GetSurfaceOutput()
+ if surface_output.HasConnectedSource():
+ connected_source = surface_output.GetConnectedSource()
+ if connected_source[0]:
+ shader_prim = connected_source[0].GetPrim()
+ textures = extract_textures_from_shader(
+ shader_prim, prop_dir
+ )
+
+ # If no textures found via USD, fall back to directory searching
+ if not textures:
+ logging.info(
+ f"No USD textures found for {segment_key}, falling back to directory search"
+ )
+ textures = find_textures_for_material(
+ prop_dir, mesh_name, material_type
+ )
+
+ # Create material info dictionary with rounded values
+ result["segments"][segment_key] = {
+ "name": material_name,
+ "opacity": opacity,
+ "material_type": material_type,
+ "semantic_usage": semantic_usage,
+ "density": round_float_to_2dp(physics_props.get("density")),
+ "dynamic_friction": round_float_to_2dp(
+ physics_props.get("dynamic_friction")
+ ),
+ "static_friction": round_float_to_2dp(
+ physics_props.get("static_friction")
+ ),
+ "restitution": round_float_to_2dp(physics_props.get("restitution")),
+ "textures": textures,
+ }
+
+ # If no materials were found but we have meshes, create default entries with textures
+ if not found_materials and mesh_prims and prop_dir:
+ logging.info(
+ f"No material bindings found for {prop_name}, creating default materials"
+ )
+
+ # Try to infer material type from prop name
+ material_type = "metal" # Default
+ if "wood" in prop_name.lower():
+ material_type = "wood"
+ elif "metal" in prop_name.lower() or "aluminum" in prop_name.lower():
+ material_type = "metal"
+ elif "plastic" in prop_name.lower():
+ material_type = "plastic"
+ elif "fabric" in prop_name.lower() or "cloth" in prop_name.lower():
+ material_type = "fabric"
+
+ # Get physics properties for this material type
+ physics_props = physics_materials.get(material_type, {})
+
+ # Process each mesh
+ for prim in mesh_prims:
+ mesh_name = prim.GetName()
+
+ # Get the full path for consistent segment key construction
+ full_path = str(prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and default prim if needed
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the common.py convention
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ # Find textures for this mesh using directory search (since no USD materials)
+ textures = find_textures_for_material(
+ prop_dir, mesh_name, material_type
+ )
+
+ # If no textures found for this specific mesh, try more general search
+ if not textures:
+ textures = find_textures_for_material(prop_dir, "", material_type)
+
+ # Create a default material info entry
+ if textures:
+ default_material_name = f"default__{material_type}__{prop_name}"
+
+ result["segments"][segment_key] = {
+ "name": default_material_name,
+ "opacity": "opaque",
+ "material_type": material_type,
+ "semantic_usage": prop_name,
+ "density": round_float_to_2dp(physics_props.get("density")),
+ "dynamic_friction": round_float_to_2dp(
+ physics_props.get("dynamic_friction")
+ ),
+ "static_friction": round_float_to_2dp(
+ physics_props.get("static_friction")
+ ),
+ "restitution": round_float_to_2dp(
+ physics_props.get("restitution")
+ ),
+ "textures": textures,
+ }
+
+ found_materials = True
+
+ # Handle case where we couldn't find any materials or textures
+ if not found_materials:
+ logging.warning(f"No materials or textures found for {prop_name}")
+
+ # Create a minimal stub entry
+ if mesh_prims:
+ # Just use the first mesh
+ prim = mesh_prims[0]
+ mesh_name = prim.GetName()
+
+ # Get the full path for consistent segment key construction
+ full_path = str(prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and default prim if needed
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the common.py convention
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ result["segments"][segment_key] = {
+ "name": f"unknown_material",
+ "opacity": "opaque",
+ "material_type": "unknown",
+ "semantic_usage": "",
+ "density": None,
+ "dynamic_friction": None,
+ "static_friction": None,
+ "restitution": None,
+ "textures": {},
+ }
+
+ # Ensure category and dataset_type present (in case older function call didn't inject)
+ if "category" not in result:
+ result["category"] = ASSET_CLASS_MAPPING.get(prop_name, "unknown")
+ if "dataset_type" not in result:
+ result["dataset_type"] = "simready"
+
+ return result
+
+ except Exception as e:
+ logging.error(f"Error extracting materials for {prop_name}: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return {"object_name": prop_name, "segments": {}}
+
+
+def extract_textures_from_shader(shader_prim, prop_dir):
+ """
+ Extract texture information directly from a shader prim.
+
+ Args:
+ shader_prim: USD shader prim
+ prop_dir: Path to prop directory for resolving relative paths
+
+ Returns:
+ dict: Dictionary of texture paths by type
+ """
+ textures = {}
+
+ if not shader_prim.IsA(UsdShade.Shader):
+ return textures
+
+ shader = UsdShade.Shader(shader_prim)
+
+ # Map of USD shader input names to our texture type names
+ texture_input_mapping = {
+ "diffuse_texture": "albedo",
+ "albedo_texture": "albedo",
+ "basecolor_texture": "albedo",
+ "normalmap_texture": "normal",
+ "normal_texture": "normal",
+ "reflectionroughness_texture": "roughness",
+ "roughness_texture": "roughness",
+ "metallic_texture": "metallic",
+ "orm_texture": "orm",
+ "emissive_texture": "emissive",
+ "opacity_texture": "opacity",
+ }
+
+ # Get all shader inputs
+ shader_inputs = shader.GetInputs()
+
+ for shader_input in shader_inputs:
+ input_name = shader_input.GetBaseName()
+
+ # Check if this input corresponds to a texture
+ texture_type = texture_input_mapping.get(input_name)
+ if texture_type:
+ # Get the connected source
+ source_info = shader_input.GetConnectedSource()
+ if source_info[0]: # If connected
+ source_prim = source_info[0].GetPrim()
+ if source_prim.IsA(UsdShade.Shader):
+ # This is a texture node - get the file path
+ texture_shader = UsdShade.Shader(source_prim)
+ file_input = texture_shader.GetInput("file")
+ if file_input:
+ texture_path = file_input.Get()
+ if texture_path:
+ # Convert to absolute path if relative
+ if not os.path.isabs(texture_path):
+ texture_path = os.path.join(prop_dir, texture_path)
+ textures[texture_type] = texture_path
+
+ # Also check direct file inputs (some shaders may have direct file paths)
+ elif input_name == "file" or input_name.endswith("_file"):
+ texture_path = shader_input.Get()
+ if texture_path:
+ # Try to infer texture type from filename
+ lower_name = os.path.basename(texture_path).lower()
+ if (
+ "albedo" in lower_name
+ or "basecolor" in lower_name
+ or "diffuse" in lower_name
+ ):
+ texture_type = "albedo"
+ elif "normal" in lower_name:
+ texture_type = "normal"
+ elif "roughness" in lower_name:
+ texture_type = "roughness"
+ elif "metallic" in lower_name:
+ texture_type = "metallic"
+ elif "orm" in lower_name:
+ texture_type = "orm"
+ elif "emissive" in lower_name:
+ texture_type = "emissive"
+ else:
+ texture_type = "albedo" # Default to albedo
+
+ # Convert to absolute path if relative
+ if not os.path.isabs(texture_path):
+ texture_path = os.path.join(prop_dir, texture_path)
+ textures[texture_type] = texture_path
+
+ return textures
+
+
+def extract_materials_from_usd_improved(usd_file_path, prop_name=None, prop_dir=None):
+ """
+ Extract material information from a USD file with improved texture handling.
+ Uses direct USD shader attribute extraction instead of directory searching.
+
+ Args:
+ usd_file_path (str): Path to the USD file
+ prop_name (str, optional): Name of the prop (directory name)
+ prop_dir (str, optional): Path to the prop directory
+
+ Returns:
+ dict: Dictionary mapping segment names to material info
+ """
+ if not os.path.exists(usd_file_path):
+ logging.error(f"USD file not found at {usd_file_path}")
+ return {"object_name": prop_name, "segments": {}}
+
+ try:
+ # Load physics materials
+ physics_materials = load_physics_materials()
+
+ # Open the USD stage
+ stage = Usd.Stage.Open(usd_file_path)
+ if not stage:
+ logging.error(f"Failed to open USD stage from {usd_file_path}")
+ return {"object_name": prop_name, "segments": {}}
+
+ # Get the default prim
+ default_prim = stage.GetDefaultPrim()
+ if not default_prim:
+ default_prim = stage.GetPrimAtPath("/")
+
+ # Create the result dictionary with prop name, dataset type, and category
+ category = ASSET_CLASS_MAPPING.get(prop_name, "unknown")
+ result = {
+ "object_name": prop_name,
+ "category": category,
+ "dataset_type": "simready",
+ "segments": {},
+ }
+
+ # Flag to track if we found any materials
+ found_materials = False
+
+ # Find all mesh prims
+ mesh_prims = []
+ for prim in Usd.PrimRange(default_prim):
+ if prim.GetTypeName() == "Mesh":
+ mesh_prims.append(prim)
+
+ if not mesh_prims:
+ logging.warning(f"No mesh prims found in {usd_file_path}")
+
+ # Process each mesh prim
+ for prim in mesh_prims:
+ mesh_name = prim.GetName()
+
+ # Get the full path for consistent segment key construction
+ full_path = str(prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and default prim if needed
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the common.py convention
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ # Get material binding
+ material_binding = UsdShade.MaterialBindingAPI(prim)
+ bound_material = None
+
+ if material_binding:
+ bound_material = material_binding.GetDirectBinding().GetMaterial()
+ if not bound_material:
+ # Try to get bound material through collections
+ material_found = False
+ for binding in material_binding.GetCollectionBindings():
+ bound_material = binding.GetMaterial()
+ if bound_material:
+ material_found = True
+ break
+
+ # If material is found, extract its properties
+ if bound_material:
+ found_materials = True
+
+ # Get material name
+ material_name = bound_material.GetPath().name
+
+ # Parse material name
+ opacity, material_type, semantic_usage = parse_material_name(
+ material_name
+ )
+
+ # Get physics properties for this material type if available
+ physics_props = physics_materials.get(material_type, {})
+
+ # Find textures for this material
+ textures = {}
+ if prop_dir:
+ textures = find_textures_for_material(
+ prop_dir, mesh_name, material_type
+ )
+
+ # Create material info dictionary with rounded values
+ result["segments"][segment_key] = {
+ "name": material_name,
+ "opacity": opacity,
+ "material_type": material_type,
+ "semantic_usage": semantic_usage,
+ "density": round_float_to_2dp(physics_props.get("density")),
+ "dynamic_friction": round_float_to_2dp(
+ physics_props.get("dynamic_friction")
+ ),
+ "static_friction": round_float_to_2dp(
+ physics_props.get("static_friction")
+ ),
+ "restitution": round_float_to_2dp(physics_props.get("restitution")),
+ "textures": textures,
+ }
+
+ # If no materials were found but we have meshes, create default entries with textures
+ if not found_materials and mesh_prims and prop_dir:
+ logging.info(
+ f"No material bindings found for {prop_name}, creating default materials"
+ )
+
+ # Try to infer material type from prop name
+ material_type = "metal" # Default
+ if "wood" in prop_name.lower():
+ material_type = "wood"
+ elif "metal" in prop_name.lower() or "aluminum" in prop_name.lower():
+ material_type = "metal"
+ elif "plastic" in prop_name.lower():
+ material_type = "plastic"
+ elif "fabric" in prop_name.lower() or "cloth" in prop_name.lower():
+ material_type = "fabric"
+
+ # Get physics properties for this material type
+ physics_props = physics_materials.get(material_type, {})
+
+ # Process each mesh
+ for prim in mesh_prims:
+ mesh_name = prim.GetName()
+
+ # Get the full path for consistent segment key construction
+ full_path = str(prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and default prim if needed
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the common.py convention
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ # Find textures for this mesh
+ textures = find_textures_for_material(
+ prop_dir, mesh_name, material_type
+ )
+
+ # If no textures found for this specific mesh, try more general search
+ if not textures:
+ textures = find_textures_for_material(prop_dir, "", material_type)
+
+ # Create a default material info entry
+ if textures:
+ default_material_name = f"default__{material_type}__{prop_name}"
+
+ result["segments"][segment_key] = {
+ "name": default_material_name,
+ "opacity": "opaque",
+ "material_type": material_type,
+ "semantic_usage": prop_name,
+ "density": round_float_to_2dp(physics_props.get("density")),
+ "dynamic_friction": round_float_to_2dp(
+ physics_props.get("dynamic_friction")
+ ),
+ "static_friction": round_float_to_2dp(
+ physics_props.get("static_friction")
+ ),
+ "restitution": round_float_to_2dp(
+ physics_props.get("restitution")
+ ),
+ "textures": textures,
+ }
+
+ found_materials = True
+
+ # Handle case where we couldn't find any materials or textures
+ if not found_materials:
+ logging.warning(f"No materials or textures found for {prop_name}")
+
+ # Create a minimal stub entry
+ if mesh_prims:
+ # Just use the first mesh
+ prim = mesh_prims[0]
+ mesh_name = prim.GetName()
+
+ # Get the full path for consistent segment key construction
+ full_path = str(prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and default prim if needed
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the common.py convention
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ result["segments"][segment_key] = {
+ "name": f"unknown_material",
+ "opacity": "opaque",
+ "material_type": "unknown",
+ "semantic_usage": "",
+ "density": None,
+ "dynamic_friction": None,
+ "static_friction": None,
+ "restitution": None,
+ "textures": {},
+ }
+
+ # Ensure category and dataset_type present (in case older function call didn't inject)
+ if "category" not in result:
+ result["category"] = ASSET_CLASS_MAPPING.get(prop_name, "unknown")
+ if "dataset_type" not in result:
+ result["dataset_type"] = "simready"
+
+ return result
+
+ except Exception as e:
+ logging.error(f"Error extracting materials for {prop_name}: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return {"object_name": prop_name, "segments": {}}
+
+
+def process_simready_objects(
+ vlm_model,
+ vlm_processor,
+ limit=None,
+ processed_objects=None,
+ output_file=None,
+ existing_results=None,
+):
+ OBJECTS_TO_PROCESS = [
+ d for d in os.listdir(PROPS_DIR) if os.path.isdir(os.path.join(PROPS_DIR, d))
+ ]
+ OBJECTS_TO_PROCESS.sort()
+
+ processed_objects = processed_objects or set()
+ existing_results = existing_results or []
+
+ # Initialize statistics
+ all_results = []
+ success_count = 0
+ failed_props = []
+ total_segments = 0
+ unique_materials = set()
+ materials_per_prop = {}
+ total_rendered_segments = 0
+ total_vlm_segments = 0
+
+ # Statistics for texture availability
+ segments_with_albedo = 0
+ segments_without_albedo = 0
+ segments_with_thumbnail_only = 0
+
+ if limit:
+ OBJECTS_TO_PROCESS = OBJECTS_TO_PROCESS[:limit]
+
+ for prop_idx, prop_name in enumerate(
+ tqdm(OBJECTS_TO_PROCESS, desc="Processing SimReady objects")
+ ):
+ # Skip objects that have already been processed
+ if prop_name in processed_objects:
+ logging.info(f"Skipping already processed object: {prop_name}")
+
+ # Find its results in existing_results to count in statistics
+ for result in existing_results:
+ if result.get("object_name") == prop_name:
+ all_results.append(result)
+ success_count += 1
+ total_segments += len(result.get("segments", {}))
+ break
+
+ continue
+
+ try:
+ full_prop_dir = os.path.join(PROPS_DIR, prop_name)
+
+ if not os.path.isdir(full_prop_dir):
+ logging.error(f"Prop directory not found at {full_prop_dir}")
+ failed_props.append(prop_name)
+ continue
+
+ # Find a USD file in the prop directory
+ try:
+ usd_file = get_usd_file_from_prop_dir(full_prop_dir)
+ logging.info(
+ f"Found USD file for {prop_name}: {os.path.basename(usd_file)}"
+ )
+ except:
+ logging.error(f"Could not find USD file for {prop_name}")
+ failed_props.append(prop_name)
+ continue
+
+ # Extract material information
+ materials_dict = extract_materials_from_usd(
+ usd_file, prop_name, full_prop_dir
+ )
+
+ # Track statistics
+ segments = materials_dict.get("segments", {})
+ total_segments += len(segments)
+
+ # Count unique materials for this prop
+ prop_materials = set()
+ for segment_key, segment_info in segments.items():
+ unique_materials.add(segment_info["name"])
+ prop_materials.add(segment_info["name"])
+
+ # Record materials per prop
+ if len(segments) > 0:
+ materials_per_prop[prop_name] = len(prop_materials)
+
+ # Determine thumbnail path from SimReady structure
+ thumb_path = os.path.join(
+ full_prop_dir,
+ ".thumbs",
+ "256x256",
+ f"{prop_name}.usd.png",
+ )
+ has_thumbnail = os.path.exists(thumb_path)
+
+ if not has_thumbnail:
+ logging.warning(f"No thumbnail found for {prop_name} at {thumb_path}")
+ # Try to find any thumbnail in the .thumbs directory
+ thumb_dir = os.path.join(full_prop_dir, ".thumbs", "256x256")
+ if os.path.exists(thumb_dir):
+ thumb_files = [
+ f for f in os.listdir(thumb_dir) if f.endswith(".png")
+ ]
+ if thumb_files:
+ thumb_path = os.path.join(thumb_dir, thumb_files[0])
+ has_thumbnail = True
+ logging.info(f"Found alternative thumbnail: {thumb_path}")
+
+ # Add to combined results if we have segments
+ os.makedirs("/tmp/vlm", exist_ok=True)
+ if len(segments) > 0:
+ # Process every segment with VLM
+ if vlm_model:
+ for segment_key, segment_info in segments.items():
+ textures = segment_info.get("textures", {})
+
+ # Log texture information for diagnostics
+ logging.info(
+ f"Segment {segment_key} has textures: {list(textures.keys())}"
+ )
+
+ has_albedo = "albedo" in textures
+ if has_albedo:
+ # Has albedo texture - render sphere and use with thumbnail
+ segments_with_albedo += 1
+ logging.info(
+ f"Rendering texture sphere for {prop_name}, segment {segment_key}"
+ )
+
+ # Set up file path for this segment's rendered sphere
+ segment_render_path = (
+ f"/tmp/vlm/texture_sphere_{prop_name}_{segment_key}.png"
+ )
+
+ try:
+ rgb_buffer = render_sphere_with_texture(
+ textures, segment_render_path
+ )
+ logging.info(f"RGB buffer shape: {rgb_buffer.shape}")
+ except Exception as e:
+ logging.error(
+ f"Error rendering texture for {segment_key}: {str(e)}"
+ )
+ segment_render_path = None
+ else:
+ # No albedo texture - just use thumbnail
+ segments_without_albedo += 1
+ segment_render_path = None
+ logging.info(
+ f"No albedo texture for {prop_name}, segment {segment_key}. Using thumbnail only."
+ )
+
+ # Always try to process with VLM, even if no texture
+ try:
+ # If we have a thumbnail but no texture, still run VLM with just the thumbnail
+ if not has_albedo and has_thumbnail:
+ segments_with_thumbnail_only += 1
+ logging.info(
+ f"Using thumbnail only for {prop_name}, segment {segment_key}"
+ )
+
+ # Don't run VLM if we have neither texture nor thumbnail
+ if not segment_render_path and not has_thumbnail:
+ logging.warning(
+ f"Skipping VLM for {segment_key} - no texture or thumbnail available"
+ )
+ continue
+
+ part1 = make_user_prompt(
+ segment_info["material_type"],
+ segment_info["opacity"],
+ segment_info["density"],
+ segment_info["dynamic_friction"],
+ segment_info["static_friction"],
+ segment_info["restitution"],
+ segment_info["semantic_usage"],
+ has_texture_sphere=segment_render_path is not None,
+ )
+
+ # Store the custom prompt in material_info
+ segment_info["user_prompt"] = part1
+
+ # Debug: Log the prompt type based on texture availability
+ if segment_render_path is not None:
+ logging.info(
+ f"Using prompt WITH texture sphere for {prop_name}, segment {segment_key}"
+ )
+ else:
+ logging.info(
+ f"Using prompt WITHOUT texture sphere for {prop_name}, segment {segment_key}"
+ )
+ logging.info(
+ f"PROMPT: {part1[:100]}..."
+ ) # Print just the beginning of the prompt
+
+ vlm_analysis = analyze_material_with_vlm(
+ segment_render_path, # This can be None, in which case only thumbnail is used
+ segment_info,
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name="simready",
+ PROMPTS=PROMPTS,
+ make_user_prompt=make_user_prompt,
+ parse_vlm_output=parse_vlm_output,
+ )
+
+ # Add VLM analysis to segment info directly
+ if vlm_analysis and "error" not in vlm_analysis:
+ # Update the segment info with VLM-derived properties
+ segment_info["vlm_analysis"] = vlm_analysis.get(
+ "vlm_analysis"
+ )
+
+ if vlm_analysis.get("youngs_modulus") is not None:
+ segment_info["youngs_modulus"] = vlm_analysis.get(
+ "youngs_modulus"
+ )
+
+ if vlm_analysis.get("poissons_ratio") is not None:
+ segment_info["poissons_ratio"] = vlm_analysis.get(
+ "poissons_ratio"
+ )
+
+ if vlm_analysis.get("density") is not None:
+ segment_info["density"] = vlm_analysis.get(
+ "density"
+ )
+
+ total_vlm_segments += 1
+ logging.info(
+ f"VLM analysis successful for {segment_key}:"
+ )
+ logging.info(
+ f" Young's modulus: {vlm_analysis.get('youngs_modulus')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {vlm_analysis.get('poissons_ratio')}"
+ )
+ if vlm_analysis.get("density") is not None:
+ logging.info(
+ f" Density: {vlm_analysis.get('density')}"
+ )
+ else:
+ logging.error(
+ f"VLM analysis failed for {segment_key}: {vlm_analysis.get('error', 'Unknown error')}"
+ )
+ except Exception as e:
+ import traceback
+
+ logging.error(
+ f"Error during VLM analysis for {segment_key}: {str(e)}"
+ )
+ logging.error(traceback.format_exc())
+
+ total_rendered_segments += 1
+
+ all_results.append(materials_dict)
+ success_count += 1
+
+ # Incremental save after each object if output file is provided
+ if output_file:
+ try:
+ with open(output_file, "w") as f:
+ import json
+ from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ UsdJsonEncoder,
+ )
+
+ # Debug save contents
+ logging.info(
+ f"Saving checkpoint with {len(all_results)} objects"
+ )
+
+ # Ensure result types are JSON serializable
+ for idx, item in enumerate(all_results):
+ if "segments" in item:
+ for seg_key, seg_info in item["segments"].items():
+ if "textures" in seg_info and isinstance(
+ seg_info["textures"], dict
+ ):
+ # Convert any non-serializable texture paths to strings
+ serializable_textures = {}
+ for tex_type, tex_path in seg_info[
+ "textures"
+ ].items():
+ serializable_textures[tex_type] = str(
+ tex_path
+ )
+ seg_info["textures"] = serializable_textures
+
+ json.dump(all_results, f, indent=4, cls=UsdJsonEncoder)
+
+ except Exception as e:
+ logging.error(f"Error saving checkpoint: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ else:
+ logging.warning(f"No segments extracted for {prop_name}")
+ failed_props.append(prop_name)
+
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error processing {prop_name}: {str(e)}")
+ logging.error(traceback.format_exc())
+ failed_props.append(prop_name)
+
+ # Log texture statistics
+ logging.info("Texture Statistics:")
+ logging.info(f" Total segments processed: {total_segments}")
+ logging.info(f" Segments with albedo textures: {segments_with_albedo}")
+ logging.info(f" Segments without albedo textures: {segments_without_albedo}")
+ logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
+ logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
+
+ return (
+ all_results,
+ len(OBJECTS_TO_PROCESS),
+ success_count,
+ failed_props,
+ total_segments,
+ total_rendered_segments,
+ total_vlm_segments,
+ list(unique_materials),
+ materials_per_prop,
+ )
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/vegetation.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/vegetation.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d37549a949e2631af230a82da31a7b04f4978c4
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/vegetation.py
@@ -0,0 +1,419 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ VEGETATION_BASE_DIR,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
+ render_sphere_with_texture,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
+ analyze_material_with_vlm,
+ parse_vlm_properties,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ extract_materials_from_usd,
+ UsdJsonEncoder,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.residential import (
+ PROMPTS,
+ make_user_prompt,
+)
+import re
+from tqdm import tqdm
+import os
+import logging
+import copy
+
+# Use the centralized parser function
+parse_vlm_output = parse_vlm_properties
+
+
+def list_vegetation_objects():
+ """
+ List all available vegetation objects in the vegetation directory.
+ """
+ usd_files = []
+ print("\nAvailable vegetation objects:")
+ for root, _, files in os.walk(VEGETATION_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_files.append(os.path.join(root, file))
+ print(f" - {os.path.basename(root)}/{file}")
+ print()
+
+
+def process_vegetation(
+ vlm_model,
+ vlm_processor,
+ limit=None,
+ processed_objects=None,
+ output_file=None,
+ existing_results=None,
+):
+ usd_files = []
+ for root, _, files in os.walk(VEGETATION_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ usd_files.append(os.path.join(root, file))
+
+ logging.info(f"Found {len(usd_files)} USD files in vegetation dataset")
+
+ # Initialize tracking sets and results
+ processed_objects = set() if processed_objects is None else processed_objects
+ existing_results = [] if existing_results is None else existing_results
+
+ # Build a set of already processed object names from existing_results
+ existing_object_names = {
+ result.get("object_name")
+ for result in existing_results
+ if "object_name" in result
+ }
+ logging.info(
+ f"Found {len(existing_object_names)} already processed objects in existing results"
+ )
+
+ # Add names from existing_results to processed_objects to avoid reprocessing
+ processed_objects.update(existing_object_names)
+
+ # Create a copy of existing_results to avoid modifying the original
+ all_results = copy.deepcopy(existing_results)
+
+ usd_files.sort()
+
+ if limit and limit > 0:
+ usd_files = usd_files[:limit]
+
+ success_count = 0
+ failed_objects = []
+ total_segments = 0
+ unique_materials = set()
+ materials_per_object = {}
+ total_rendered_segments = 0
+ total_vlm_segments = 0
+
+ # Count total segments from existing results
+ for result in existing_results:
+ total_segments += len(result.get("segments", {}))
+
+ # Statistics for texture availability
+ segments_with_texture = 0
+ segments_without_texture = 0
+ segments_with_thumbnail_only = 0
+
+ # Track processed files to avoid duplicates from the same directory
+ processed_files = set()
+
+ for usd_file in tqdm(usd_files, desc=f"Processing vegetation dataset"):
+ # Extract object name from path
+ object_name = os.path.basename(os.path.dirname(usd_file))
+
+ # Skip if we already processed this exact file
+ if usd_file in processed_files:
+ continue
+
+ # Skip objects that have already been processed
+ if object_name in processed_objects:
+ logging.info(f"Skipping already processed object: {object_name}")
+ continue
+
+ try:
+ directory = os.path.dirname(usd_file)
+
+ # Extract material information
+ result = extract_materials_from_usd(usd_file, "vegetation")
+
+ if result:
+ # Add to processed_files to avoid duplicates
+ processed_files.add(usd_file)
+
+ # Track statistics
+ segments = result.get("segments", {})
+ total_segments += len(segments)
+
+ # Remove object_name and note fields from segments
+ for segment_key, segment_info in segments.items():
+ if "object_name" in segment_info:
+ del segment_info["object_name"]
+ if "note" in segment_info:
+ del segment_info["note"]
+
+ # Count unique materials for this object
+ object_materials = set()
+ for segment_name, segment_info in segments.items():
+ material_name = segment_info.get("material_type", "unknown")
+ unique_materials.add(material_name)
+ object_materials.add(material_name)
+
+ # Record materials per object
+ if len(segments) > 0:
+ materials_per_object[object_name] = len(object_materials)
+
+ # Process VLM for each segment if VLM model is provided
+ os.makedirs("/tmp/vlm", exist_ok=True)
+
+ if vlm_model and len(segments) > 0:
+ for segment_name, segment_info in segments.items():
+ textures = segment_info.get("textures", {})
+
+ # Log texture information for diagnostics
+ logging.info(
+ f"Segment {segment_name} has textures: {list(textures.keys())}"
+ )
+
+ # Check if we have either a normal or roughness texture for rendering
+ has_texture = (
+ "normal" in textures
+ or "roughness" in textures
+ or "diffuse" in textures
+ )
+ if has_texture:
+ # Has texture - render sphere and use with thumbnail
+ segments_with_texture += 1
+ logging.info(
+ f"Rendering texture sphere for {object_name}, segment {segment_name}"
+ )
+
+ # Set up file path for this segment's rendered sphere
+ segment_render_path = f"/tmp/vlm/texture_sphere_{object_name}_{segment_name}.png"
+
+ # Render the textured sphere
+ try:
+ rgb_buffer = render_sphere_with_texture(
+ textures, segment_render_path
+ )
+ logging.info(f"RGB buffer shape: {rgb_buffer.shape}")
+ except Exception as e:
+ logging.error(
+ f"Error rendering texture for {segment_name}: {str(e)}"
+ )
+ segment_render_path = None
+ else:
+ # No texture - just use thumbnail
+ segments_without_texture += 1
+ segment_render_path = None
+ logging.info(
+ f"No texture for {object_name}, segment {segment_name}. Using thumbnail only."
+ )
+
+ try:
+ # Get thumbnail path if available
+ thumb_path = None
+ # For vegetation dataset, thumbnails are in .thumbs/256x256 directory
+ thumb_dir = os.path.join(
+ os.path.dirname(usd_file), ".thumbs", "256x256"
+ )
+
+ has_thumbnail = False
+ if os.path.exists(thumb_dir):
+ # Try to find a thumbnail matching the USD filename
+ usd_filename = os.path.basename(usd_file)
+ thumb_candidates = [
+ # Regular thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.png"),
+ # Auto-generated thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.auto.png"),
+ ]
+
+ for candidate in thumb_candidates:
+ if os.path.exists(candidate):
+ thumb_path = candidate
+ has_thumbnail = True
+ logging.info(f"Found thumbnail: {thumb_path}")
+ break
+
+ # If we have a thumbnail but no texture, still run VLM with just the thumbnail
+ if not has_texture and has_thumbnail:
+ segments_with_thumbnail_only += 1
+ logging.info(
+ f"Using thumbnail only for {object_name}, segment {segment_name}"
+ )
+
+ # Don't run VLM if we have neither texture nor thumbnail
+ if not segment_render_path and not has_thumbnail:
+ logging.warning(
+ f"Skipping VLM for {segment_name} - no texture or thumbnail available"
+ )
+ continue
+
+ # Set semantic usage to segment name but don't store in segment data
+ semantic_usage = segment_name
+ temp_object_name = object_name
+
+ # Create custom prompt based on texture availability
+ custom_prompt = make_user_prompt(
+ segment_info["material_type"],
+ semantic_usage,
+ temp_object_name,
+ has_texture_sphere=segment_render_path is not None,
+ )
+
+ # Store the prompt for VLM but not object_name
+ segment_info["user_prompt"] = custom_prompt
+
+ # Debug: Log the prompt type based on texture availability
+ if segment_render_path is not None:
+ logging.info(
+ f"Using prompt WITH texture sphere for {object_name}, segment {segment_name}"
+ )
+ else:
+ logging.info(
+ f"Using prompt WITHOUT texture sphere for {object_name}, segment {segment_name}"
+ )
+ logging.info(
+ f"PROMPT: {custom_prompt[:100]}..."
+ ) # Print just the beginning of the prompt
+
+ # Create a temporary segment_info with object_name for VLM but don't save to result
+ temp_segment_info = segment_info.copy()
+ temp_segment_info["semantic_usage"] = semantic_usage
+ temp_segment_info["object_name"] = temp_object_name
+
+ vlm_analysis = analyze_material_with_vlm(
+ segment_render_path,
+ temp_segment_info, # Use temporary copy with object_name
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name="vegetation",
+ PROMPTS=PROMPTS,
+ make_user_prompt=make_user_prompt,
+ parse_vlm_output=parse_vlm_output,
+ )
+
+ # Add VLM analysis to segment info
+ if vlm_analysis and "error" not in vlm_analysis:
+ segment_info["vlm_analysis"] = vlm_analysis.get(
+ "vlm_analysis"
+ )
+
+ if vlm_analysis.get("youngs_modulus") is not None:
+ segment_info["youngs_modulus"] = vlm_analysis.get(
+ "youngs_modulus"
+ )
+
+ if vlm_analysis.get("poissons_ratio") is not None:
+ segment_info["poissons_ratio"] = vlm_analysis.get(
+ "poissons_ratio"
+ )
+
+ if vlm_analysis.get("density") is not None:
+ segment_info["density"] = vlm_analysis.get(
+ "density"
+ )
+
+ total_vlm_segments += 1
+ logging.info(f"VLM analysis successful:")
+ logging.info(
+ f" Young's modulus: {vlm_analysis.get('youngs_modulus')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {vlm_analysis.get('poissons_ratio')}"
+ )
+ logging.info(
+ f" Density: {vlm_analysis.get('density')}"
+ )
+ else:
+ logging.error(
+ f"VLM analysis failed: {vlm_analysis.get('error', 'Unknown error')}"
+ )
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error during VLM analysis: {str(e)}")
+ logging.error(traceback.format_exc())
+
+ total_rendered_segments += 1
+
+ all_results.append(result) # Add to our local copy of results
+ processed_objects.add(object_name) # Mark as processed
+
+ # Incremental save after each object if output file is provided
+ if output_file:
+ try:
+ with open(output_file, "w") as f:
+ import json
+
+ # Debug save contents
+ logging.info(
+ f"Saving checkpoint with {len(all_results)} objects"
+ )
+
+ # Ensure result types are JSON serializable
+ for idx, item in enumerate(all_results):
+ if "segments" in item:
+ for seg_key, seg_info in item["segments"].items():
+ # Remove object_name and note fields if they exist
+ if "object_name" in seg_info:
+ del seg_info["object_name"]
+ if "note" in seg_info:
+ del seg_info["note"]
+
+ if "textures" in seg_info and isinstance(
+ seg_info["textures"], dict
+ ):
+ # Convert any non-serializable texture paths to strings
+ serializable_textures = {}
+ for tex_type, tex_path in seg_info[
+ "textures"
+ ].items():
+ serializable_textures[tex_type] = str(
+ tex_path
+ )
+ seg_info["textures"] = serializable_textures
+
+ json.dump(all_results, f, indent=4, cls=UsdJsonEncoder)
+
+ except Exception as e:
+ logging.error(f"Error saving checkpoint: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+
+ success_count += 1
+ else:
+ logging.warning(f"No material information extracted for {usd_file}")
+ failed_objects.append(object_name)
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error processing {usd_file}: {str(e)}")
+ logging.error(traceback.format_exc())
+ failed_objects.append(os.path.basename(os.path.dirname(usd_file)))
+
+ # Log texture statistics
+ logging.info("Texture Statistics:")
+ logging.info(f" Total segments processed: {total_segments}")
+ logging.info(f" Segments with textures: {segments_with_texture}")
+ logging.info(f" Segments without textures: {segments_without_texture}")
+ logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
+ logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
+
+ # Convert materials_per_object to list format for consistency with simready
+ materials_per_object_list = []
+ for obj_name, count in materials_per_object.items():
+ materials_per_object_list.append(obj_name)
+
+ return (
+ all_results,
+ len(usd_files),
+ success_count,
+ failed_objects,
+ total_segments,
+ total_rendered_segments,
+ total_vlm_segments,
+ list(unique_materials),
+ materials_per_object_list,
+ )
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/main.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..efcea87c03b4e718694f3632977c23c9e479d0e2
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/main.py
@@ -0,0 +1,1508 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import sys
+import logging
+import os
+import json
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.simready import (
+ list_simready_objects,
+ extract_materials_from_usd as simready_extract_materials,
+ get_usd_file_from_prop_dir as simready_get_usd_file,
+ PROPS_DIR as SIMREADY_PROPS_DIR,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.residential import (
+ list_residential_objects,
+ make_user_prompt as residential_make_user_prompt,
+ PROMPTS as RESIDENTIAL_PROMPTS,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.commercial import (
+ list_commercial_objects,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.vegetation import (
+ list_vegetation_objects,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ COMMERCIAL_BASE_DIR,
+ RESIDENTIAL_BASE_DIR,
+ VEGETATION_BASE_DIR,
+ load_material_ranges,
+ find_reference_materials,
+ parse_numerical_range_str,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
+ load_vlm_model,
+ analyze_material_with_vlm,
+ parse_vlm_properties,
+)
+from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
+ render_sphere_with_texture,
+)
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ UsdJsonEncoder,
+ extract_materials_from_usd as common_extract_materials,
+)
+import polyscope as ps
+import copy
+from tqdm import tqdm
+import shutil
+
+ps.set_allow_headless_backends(True) # enable headless backends
+ps.init()
+
+
+def load_existing_results(output_file):
+ """
+ Load existing results from a JSON file.
+
+ Args:
+ output_file (str): Path to the JSON file
+
+ Returns:
+ tuple: (results list, set of already processed object names, set of already processed file paths)
+ """
+ try:
+ if (
+ os.path.exists(output_file) and os.path.getsize(output_file) > 10
+ ): # Ensure file has some content
+ with open(output_file, "r") as f:
+ existing_results = json.load(f)
+
+ if not isinstance(existing_results, list):
+ logging.warning(
+ f"Expected list in results file, found {type(existing_results)}"
+ )
+ existing_results = []
+
+ # Create a set of already processed object names for efficient lookup
+ processed_objects = set()
+ processed_file_paths = set() # Also track file paths
+
+ for result in existing_results:
+ obj_name = result.get("object_name", "")
+ file_path = result.get("file_path", "")
+
+ if obj_name:
+ processed_objects.add(obj_name)
+ if file_path:
+ processed_file_paths.add(file_path)
+
+ logging.info(
+ f"Loaded {len(existing_results)} existing results from {output_file}"
+ )
+ logging.info(
+ f"Found {len(processed_objects)} already processed objects and {len(processed_file_paths)} file paths"
+ )
+
+ # Debug - print each processed object
+ if processed_objects:
+ logging.info(f"Processed objects: {', '.join(processed_objects)}")
+
+ return existing_results, processed_objects, processed_file_paths
+ except Exception as e:
+ logging.error(f"Error loading existing results: {str(e)}")
+
+ return [], set(), set()
+
+
+def append_result_to_file(result, all_results, output_file):
+ """
+ Append a newly processed result to the output file.
+
+ Args:
+ result: The new result to append
+ all_results: The full list of results (includes the new result)
+ output_file: Path to the output file
+
+ Returns:
+ bool: Whether the append was successful
+ """
+ try:
+ with open(output_file, "w") as f:
+ json.dump(all_results, f, indent=4, cls=UsdJsonEncoder)
+
+ # Verify file was written successfully
+ if os.path.exists(output_file) and os.path.getsize(output_file) > 10:
+ logging.info(
+ f"Updated file with new object: {result.get('object_name', 'unknown')}"
+ )
+ logging.info(
+ f"File size: {os.path.getsize(output_file)} bytes, contains {len(all_results)} objects"
+ )
+ return True
+ else:
+ logging.error(f"Failed to write updated file - file empty or missing")
+ return False
+ except Exception as e:
+ logging.error(f"Error appending result: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return False
+
+
+def analyze_material_with_retry(
+ segment_key,
+ segment_render_path,
+ segment_info,
+ vlm_model,
+ vlm_processor,
+ thumb_path,
+ dataset_name,
+ prompts,
+ make_user_prompt_func,
+ parse_vlm_output_func,
+ custom_prompt=None,
+ temp_segment_info=None,
+):
+ """
+ Analyze a material with VLM, with retry logic to ensure we get all necessary values.
+
+ Args:
+ segment_key: Key of the segment being analyzed
+ segment_render_path: Path to the rendered sphere image
+ segment_info: Info about the segment
+ vlm_model: VLM model for analysis
+ vlm_processor: VLM processor for analysis
+ thumb_path: Path to thumbnail image
+ dataset_name: Name of dataset
+ prompts: Prompt templates to use
+ make_user_prompt_func: Function to generate user prompt
+ parse_vlm_output_func: Function to parse VLM output
+ custom_prompt: Optional custom prompt already generated
+ temp_segment_info: Optional temporary segment info
+
+ Returns:
+ Segment info with analysis results added
+ """
+ # First attempt at VLM analysis
+ vlm_analysis = analyze_material_with_vlm(
+ segment_render_path,
+ segment_info if temp_segment_info is None else temp_segment_info,
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name=dataset_name,
+ PROMPTS=prompts,
+ make_user_prompt=make_user_prompt_func,
+ parse_vlm_output=parse_vlm_output_func,
+ )
+
+ # The base prompt to use for retries
+ base_prompt = (
+ custom_prompt
+ if custom_prompt is not None
+ else segment_info.get("user_prompt", "")
+ )
+
+ # Check if we got successful analysis
+ if vlm_analysis:
+ # Add VLM analysis to segment info
+ segment_info["vlm_analysis"] = vlm_analysis.get("vlm_analysis")
+
+ # Check for missing values
+ missing_values = []
+ if vlm_analysis.get("youngs_modulus") is not None:
+ segment_info["youngs_modulus"] = vlm_analysis.get("youngs_modulus")
+ else:
+ missing_values.append("Young's modulus")
+
+ if vlm_analysis.get("poissons_ratio") is not None:
+ segment_info["poissons_ratio"] = vlm_analysis.get("poissons_ratio")
+ else:
+ missing_values.append("Poisson's ratio")
+
+ if vlm_analysis.get("density") is not None:
+ segment_info["density"] = vlm_analysis.get("density")
+ else:
+ missing_values.append("density")
+
+ # If any values are missing, retry with stricter instructions
+ if missing_values and len(missing_values) > 0:
+ logging.warning(
+ f"Missing values for {segment_key}: {', '.join(missing_values)}. Retrying with stricter instructions."
+ )
+
+ # Create a stronger prompt emphasizing the need for these values
+ stricter_prompt = (
+ base_prompt
+ + "\n\nIMPORTANT: You MUST provide numerical values for Young's modulus, Poisson's ratio, and density. Do not use 'unknown' or any non-numeric values."
+ )
+
+ # Make a temporary copy with the stricter prompt
+ temp_segment_info_strict = (temp_segment_info or segment_info).copy()
+ temp_segment_info_strict["user_prompt"] = stricter_prompt
+
+ # Retry with stricter instructions
+ stricter_prompts = copy.deepcopy(prompts)
+ stricter_prompts["query_prompt"] = (
+ prompts["query_prompt"]
+ + "\n\nCRITICAL: You MUST provide NUMERICAL estimates for ALL properties regardless of uncertainty. Make your best estimate even if uncertain."
+ )
+
+ vlm_analysis_retry = analyze_material_with_vlm(
+ segment_render_path,
+ temp_segment_info_strict,
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name=dataset_name,
+ PROMPTS=stricter_prompts,
+ make_user_prompt=make_user_prompt_func,
+ parse_vlm_output=parse_vlm_output_func,
+ )
+
+ if vlm_analysis_retry and "error" not in vlm_analysis_retry:
+ # Update missing values from retry
+ for missing_value in missing_values:
+ if (
+ missing_value == "Young's modulus"
+ and vlm_analysis_retry.get("youngs_modulus") is not None
+ ):
+ segment_info["youngs_modulus"] = vlm_analysis_retry.get(
+ "youngs_modulus"
+ )
+ logging.info(
+ f"Successfully extracted Young's modulus on retry: {vlm_analysis_retry.get('youngs_modulus')}"
+ )
+
+ if (
+ missing_value == "Poisson's ratio"
+ and vlm_analysis_retry.get("poissons_ratio") is not None
+ ):
+ segment_info["poissons_ratio"] = vlm_analysis_retry.get(
+ "poissons_ratio"
+ )
+ logging.info(
+ f"Successfully extracted Poisson's ratio on retry: {vlm_analysis_retry.get('poissons_ratio')}"
+ )
+
+ if (
+ missing_value == "density"
+ and vlm_analysis_retry.get("density") is not None
+ ):
+ segment_info["density"] = vlm_analysis_retry.get("density")
+ logging.info(
+ f"Successfully extracted density on retry: {vlm_analysis_retry.get('density')}"
+ )
+
+ # Add the retry analysis to segment info
+ segment_info["vlm_analysis_retry"] = vlm_analysis_retry.get(
+ "vlm_analysis"
+ )
+ else:
+ logging.error(f"VLM retry analysis failed for {segment_key}")
+
+ logging.info(f"VLM analysis successful for {segment_key}:")
+ logging.info(
+ f" Young's modulus: {segment_info.get('youngs_modulus', 'Not extracted')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {segment_info.get('poissons_ratio', 'Not extracted')}"
+ )
+ logging.info(f" Density: {segment_info.get('density', 'Not extracted')}")
+ else:
+ # First attempt failed completely
+ logging.error(
+ f"VLM analysis failed for {segment_key}: {vlm_analysis.get('error', 'Unknown error')}"
+ )
+
+ # Try again with stricter instructions
+ logging.warning(
+ f"First VLM analysis failed for {segment_key}. Retrying with stricter instructions."
+ )
+
+ # Create a stronger prompt emphasizing the need for a successful analysis
+ stricter_prompt = (
+ base_prompt
+ + "\n\nIMPORTANT: You MUST provide numerical values for ALL properties. Make your best estimate even with uncertainty."
+ )
+
+ # Make a temporary copy with the stricter prompt
+ temp_segment_info_strict = (temp_segment_info or segment_info).copy()
+ temp_segment_info_strict["user_prompt"] = stricter_prompt
+
+ # Retry with stricter instructions
+ stricter_prompts = copy.deepcopy(prompts)
+ stricter_prompts["query_prompt"] = (
+ prompts["query_prompt"]
+ + "\n\nCRITICAL: You MUST provide NUMERICAL estimates for ALL properties regardless of uncertainty. Make your best estimate even if uncertain."
+ )
+
+ vlm_analysis_retry = analyze_material_with_vlm(
+ segment_render_path,
+ temp_segment_info_strict,
+ vlm_model,
+ vlm_processor,
+ thumbnail_path=thumb_path,
+ dataset_name=dataset_name,
+ PROMPTS=stricter_prompts,
+ make_user_prompt=make_user_prompt_func,
+ parse_vlm_output=parse_vlm_output_func,
+ )
+
+ if vlm_analysis_retry and "error" not in vlm_analysis_retry:
+ # Use the retry values
+ segment_info["vlm_analysis"] = vlm_analysis_retry.get("vlm_analysis")
+
+ if vlm_analysis_retry.get("youngs_modulus") is not None:
+ segment_info["youngs_modulus"] = vlm_analysis_retry.get(
+ "youngs_modulus"
+ )
+
+ if vlm_analysis_retry.get("poissons_ratio") is not None:
+ segment_info["poissons_ratio"] = vlm_analysis_retry.get(
+ "poissons_ratio"
+ )
+
+ if vlm_analysis_retry.get("density") is not None:
+ segment_info["density"] = vlm_analysis_retry.get("density")
+
+ logging.info(f"VLM retry analysis successful for {segment_key}:")
+ logging.info(
+ f" Young's modulus: {segment_info.get('youngs_modulus', 'Not extracted')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {segment_info.get('poissons_ratio', 'Not extracted')}"
+ )
+ logging.info(f" Density: {segment_info.get('density', 'Not extracted')}")
+ else:
+ logging.error(f"VLM retry analysis also failed for {segment_key}")
+
+ # Determine if we have extracted all required properties
+ required_props = ["youngs_modulus", "poissons_ratio", "density"]
+ have_all_props = all(segment_info.get(p) is not None for p in required_props)
+
+ # If any property is still missing, fall back to reference CSV values
+ if not have_all_props:
+ try:
+ material_type = segment_info.get("material_type", "")
+
+ # Load material database and find closest reference
+ material_db = load_material_ranges()
+ reference_materials = find_reference_materials(
+ material_db, material_type, max_matches=1
+ )
+
+ if reference_materials:
+ ref = reference_materials[0]
+
+ # Young's modulus (range is in GPa -> convert to Pa)
+ if segment_info.get("youngs_modulus") is None:
+ try:
+ y_min, y_max = parse_numerical_range_str(ref["youngs"])
+ ym_gpa = (y_min + y_max) / 2 if y_min != y_max else y_min
+ segment_info["youngs_modulus"] = ym_gpa * 1e9 # Pa
+ except Exception as e:
+ logging.error(
+ f"Error parsing Young's modulus range for fallback: {e}"
+ )
+
+ # Poisson's ratio
+ if segment_info.get("poissons_ratio") is None:
+ try:
+ p_min, p_max = parse_numerical_range_str(ref["poisson"])
+ segment_info["poissons_ratio"] = (
+ (p_min + p_max) / 2 if p_min != p_max else p_min
+ )
+ except Exception as e:
+ logging.error(
+ f"Error parsing Poisson's ratio range for fallback: {e}"
+ )
+
+ # Density (already in kg/m^3)
+ if segment_info.get("density") is None:
+ try:
+ d_min, d_max = parse_numerical_range_str(ref["density"])
+ segment_info["density"] = (
+ (d_min + d_max) / 2 if d_min != d_max else d_min
+ )
+ except Exception as e:
+ logging.error(f"Error parsing density range for fallback: {e}")
+
+ # Annotate analysis field
+ fallback_note = f"FALLBACK: Used reference material '{ref['name']}' values from CSV."
+ segment_info["vlm_analysis"] = (
+ segment_info.get("vlm_analysis", "") + "\n" + fallback_note
+ ).strip()
+
+ logging.warning(
+ f"{segment_key}: Filled missing properties using reference material '{ref['name']}'."
+ )
+ else:
+ logging.warning(
+ f"{segment_key}: No reference material match found for type '{material_type}'. Unable to fill missing properties."
+ )
+ except Exception as e:
+ logging.error(
+ f"{segment_key}: Error while applying reference material fallback โ {e}"
+ )
+
+ # Final success evaluation after fallback
+ have_all_props = all(segment_info.get(p) is not None for p in required_props)
+
+ return segment_info, have_all_props
+
+
+def process_object(
+ usd_file,
+ dataset_type,
+ object_name,
+ vlm_model,
+ vlm_processor,
+ extract_func,
+ all_results,
+ output_file,
+ processed_objects,
+):
+ """
+ Process a single object and update results.
+
+ Args:
+ usd_file: Path to the USD filengc.nvidia.com
+ tuple: (success, result_dict, segments_count, rendered_segments_count, vlm_segments_count, unique_materials)
+ """
+ # Skip if already processed
+ if object_name in processed_objects:
+ logging.info(f"Skipping already processed object: {object_name}")
+ return False, None, 0, 0, 0, set()
+
+ try:
+ # Extract material information
+ result = extract_func(usd_file, dataset_type)
+
+ if not result or not result.get("segments", {}):
+ logging.warning(f"No material information extracted for {usd_file}")
+ return False, None, 0, 0, 0, set()
+
+ # Process every segment with VLM
+ segments = result.get("segments", {})
+ segment_count = len(segments)
+ rendered_count = 0
+ vlm_count = 0
+ unique_materials = set()
+
+ if vlm_model and segment_count > 0:
+ # Process VLM for each segment - implementation varies by dataset
+ # This would be dataset-specific code...
+ pass
+
+ # Add to results
+ all_results.append(result)
+ processed_objects.add(object_name)
+
+ # Write incremental update
+ if output_file:
+ append_result_to_file(result, all_results, output_file)
+
+ return True, result, segment_count, rendered_count, vlm_count, unique_materials
+
+ except Exception as e:
+ logging.error(f"Error processing {usd_file}: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return False, None, 0, 0, 0, set()
+
+
+def process_dataset(
+ dataset_type,
+ vlm_model,
+ vlm_processor,
+ limit=None,
+ output_file=None,
+ dry_run=False,
+ force_reprocess=False,
+ existing_results=None,
+ processed_objects=None,
+ processed_file_paths=None,
+):
+ """
+ Process all objects in a dataset.
+
+ Args:
+ dataset_type: Type of dataset (simready, residential, commercial, vegetation)
+ vlm_model: VLM model for analysis
+ vlm_processor: VLM processor for analysis
+ limit: Maximum number of objects to process
+ output_file: Path to output file for incremental updates
+ dry_run: If True, only process a few objects
+ force_reprocess: If True, reprocess objects even if they exist in output_file
+ existing_results: List of existing results to start with
+ processed_objects: Set of already processed object names
+ processed_file_paths: Set of already processed file paths
+
+ Returns:
+ Tuple of results and statistics
+ """
+ # Initialize tracking sets and results
+ processed_objects = set() if processed_objects is None else processed_objects
+ processed_file_paths = (
+ set() if processed_file_paths is None else processed_file_paths
+ )
+ existing_results = [] if existing_results is None else existing_results
+
+ # Build a set of already processed object names from existing_results
+ existing_object_names = {
+ result.get("object_name")
+ for result in existing_results
+ if "object_name" in result
+ }
+ existing_file_paths = {
+ result.get("file_path") for result in existing_results if "file_path" in result
+ }
+
+ logging.info(
+ f"Found {len(existing_object_names)} already processed objects in existing results"
+ )
+ logging.info(
+ f"Found {len(existing_file_paths)} already processed file paths in existing results"
+ )
+
+ # If not forcing reprocess, add names from existing_results to processed_objects
+ if not force_reprocess:
+ processed_objects.update(existing_object_names)
+ processed_file_paths.update(existing_file_paths)
+
+ # Create a copy of existing_results to avoid modifying the original
+ all_results = copy.deepcopy(existing_results)
+
+ # Get the list of objects to process based on dataset type
+ objects_to_process = []
+
+ if dataset_type == "simready":
+ # For SimReady, get the list of prop directories
+ if not os.path.isdir(SIMREADY_PROPS_DIR):
+ logging.error(f"SimReady props directory not found at {SIMREADY_PROPS_DIR}")
+ return all_results, 0, 0, [], 0, 0, 0, [], {}
+
+ objects_to_process = [
+ d
+ for d in os.listdir(SIMREADY_PROPS_DIR)
+ if os.path.isdir(os.path.join(SIMREADY_PROPS_DIR, d))
+ ]
+ objects_to_process.sort()
+ elif dataset_type == "residential":
+ # For Residential, get the list of USD files
+ objects_to_process = []
+ for root, _, files in os.walk(RESIDENTIAL_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ objects_to_process.append(
+ (
+ os.path.join(root, file),
+ os.path.basename(os.path.dirname(os.path.join(root, file))),
+ )
+ )
+ elif dataset_type == "commercial":
+ # For Commercial, get the list of USD files
+ objects_to_process = []
+ for root, _, files in os.walk(COMMERCIAL_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ objects_to_process.append(
+ (
+ os.path.join(root, file),
+ os.path.basename(os.path.dirname(os.path.join(root, file))),
+ )
+ )
+ elif dataset_type == "vegetation":
+ # For Vegetation, get the list of USD files
+ objects_to_process = []
+ for root, _, files in os.walk(VEGETATION_BASE_DIR):
+ for file in files:
+ if file.endswith(".usd") and not os.path.basename(root).startswith("."):
+ objects_to_process.append(
+ (
+ os.path.join(root, file),
+ os.path.basename(os.path.dirname(os.path.join(root, file))),
+ )
+ )
+ else:
+ logging.error(f"Unknown dataset type: {dataset_type}")
+ return all_results, 0, 0, [], 0, 0, 0, [], {}
+
+ # Limit the number of objects to process if specified
+ if limit and limit > 0:
+ if isinstance(objects_to_process[0], tuple):
+ # For non-SimReady datasets, objects_to_process contains tuples of (path, name)
+ logging.info(
+ f"Limiting to {limit} objects out of {len(objects_to_process)}"
+ )
+ objects_to_process = objects_to_process[:limit]
+ else:
+ # For SimReady, objects_to_process contains just names
+ logging.info(
+ f"Limiting to {limit} objects out of {len(objects_to_process)}"
+ )
+ objects_to_process = objects_to_process[:limit]
+
+ # Filter out already processed objects before we even start
+ objects_to_process_filtered = []
+ for obj_info in objects_to_process:
+ if dataset_type == "simready":
+ # For SimReady, obj_info is the prop name
+ prop_name = obj_info
+ object_name = prop_name
+
+ # Skip if already processed and not forcing reprocessing
+ if object_name in processed_objects and not force_reprocess:
+ logging.info(f"Filtering out already processed object: {object_name}")
+ continue
+ else:
+ # For other datasets, obj_info is a tuple of (path, name)
+ usd_file, object_name = obj_info
+
+ # Skip if already processed by file path or name and not forcing reprocessing
+ if usd_file in processed_file_paths and not force_reprocess:
+ logging.info(f"Filtering out already processed file: {usd_file}")
+ continue
+
+ if object_name in processed_objects and not force_reprocess:
+ logging.info(f"Filtering out already processed object: {object_name}")
+ continue
+
+ # If we get here, add the object to the filtered list
+ objects_to_process_filtered.append(obj_info)
+
+ # Update the objects to process list
+ logging.info(
+ f"Filtered {len(objects_to_process) - len(objects_to_process_filtered)} already processed objects"
+ )
+ logging.info(f"Processing {len(objects_to_process_filtered)} remaining objects")
+ objects_to_process = objects_to_process_filtered
+
+ # Initialize statistics
+ success_count = 0
+ failed_objects = []
+ total_segments = 0
+ total_rendered_segments = 0
+ total_vlm_segments = 0
+ unique_materials = set()
+ materials_per_object = {}
+
+ # Count total segments from existing results
+ for result in existing_results:
+ segments = result.get("segments", {})
+ total_segments += len(segments)
+
+ # Count unique materials
+ for segment_key, segment_info in segments.items():
+ material_name = segment_info.get("name", "")
+ if material_name:
+ unique_materials.add(material_name)
+
+ # Statistics for texture availability
+ segments_with_texture = 0
+ segments_without_texture = 0
+ segments_with_thumbnail_only = 0
+ segments_text_only_vlm = 0 # New: segments with no images but VLM analysis
+
+ # Track processed files to avoid duplicates
+ processed_files = set()
+
+ # Process each object
+ for obj_idx, obj_info in enumerate(
+ tqdm(objects_to_process, desc=f"Processing {dataset_type} dataset")
+ ):
+ if dataset_type == "simready":
+ # For SimReady, obj_info is the prop name
+ prop_name = obj_info
+ object_name = prop_name
+
+ try:
+ # Get the full prop directory path
+ full_prop_dir = os.path.join(SIMREADY_PROPS_DIR, prop_name)
+
+ if not os.path.isdir(full_prop_dir):
+ logging.error(f"Prop directory not found at {full_prop_dir}")
+ failed_objects.append(prop_name)
+ continue
+
+ # Find a USD file in the prop directory
+ try:
+ usd_file = simready_get_usd_file(full_prop_dir)
+ logging.info(
+ f"Found USD file for {prop_name}: {os.path.basename(usd_file)}"
+ )
+ except:
+ logging.error(f"Could not find USD file for {prop_name}")
+ failed_objects.append(prop_name)
+ continue
+
+ # Extract material information
+ materials_dict = simready_extract_materials(
+ usd_file, prop_name, full_prop_dir
+ )
+
+ # Ensure file_path is properly stored
+ if "file_path" not in materials_dict or not materials_dict["file_path"]:
+ materials_dict["file_path"] = usd_file
+
+ # Add to tracking
+ processed_file_paths.add(usd_file)
+
+ # Track statistics
+ segments = materials_dict.get("segments", {})
+ total_segments += len(segments)
+
+ # Count unique materials for this prop
+ prop_materials = set()
+ for segment_key, segment_info in segments.items():
+ unique_materials.add(segment_info["name"])
+ prop_materials.add(segment_info["name"])
+
+ # Record materials per prop
+ if len(segments) > 0:
+ materials_per_object[prop_name] = len(prop_materials)
+
+ # Determine thumbnail path from SimReady structure
+ thumb_path = os.path.join(
+ full_prop_dir,
+ ".thumbs",
+ "256x256",
+ f"{prop_name}.usd.png",
+ )
+ has_thumbnail = os.path.exists(thumb_path)
+
+ if not has_thumbnail:
+ logging.warning(
+ f"No thumbnail found for {prop_name} at {thumb_path}"
+ )
+ # Try to find any thumbnail in the .thumbs directory
+ thumb_dir = os.path.join(full_prop_dir, ".thumbs", "256x256")
+ if os.path.exists(thumb_dir):
+ thumb_files = [
+ f for f in os.listdir(thumb_dir) if f.endswith(".png")
+ ]
+ if thumb_files:
+ thumb_path = os.path.join(thumb_dir, thumb_files[0])
+ has_thumbnail = True
+ logging.info(f"Found alternative thumbnail: {thumb_path}")
+
+ # Add to combined results if we have segments
+ os.makedirs("/tmp/vlm", exist_ok=True)
+
+ if len(segments) > 0:
+ # Process every segment with VLM
+ if vlm_model:
+ for segment_key, segment_info in segments.items():
+ textures = segment_info.get("textures", {})
+
+ # Log texture information for diagnostics
+ logging.info(
+ f"Segment {segment_key} has textures: {list(textures.keys())}"
+ )
+
+ has_albedo = "albedo" in textures
+ if has_albedo:
+ # Has albedo texture - render sphere and use with thumbnail
+ segments_with_texture += 1
+ logging.info(
+ f"Rendering texture sphere for {prop_name}, segment {segment_key}"
+ )
+
+ # Set up file path for this segment's rendered sphere
+ segment_render_path = f"/tmp/vlm/texture_sphere_{prop_name}_{segment_key}.png"
+
+ try:
+ rgb_buffer = render_sphere_with_texture(
+ textures, segment_render_path
+ )
+ logging.info(
+ f"RGB buffer shape: {rgb_buffer.shape}"
+ )
+ except Exception as e:
+ logging.error(
+ f"Error rendering texture for {segment_key}: {str(e)}"
+ )
+ segment_render_path = None
+ else:
+ # No albedo texture - just use thumbnail
+ segments_without_texture += 1
+ segment_render_path = None
+ logging.info(
+ f"No albedo texture for {prop_name}, segment {segment_key}. Using thumbnail only."
+ )
+
+ # Always try to process with VLM, even if no texture
+ try:
+ # If we have a thumbnail but no texture, still run VLM with just the thumbnail
+ if not has_albedo and has_thumbnail:
+ segments_with_thumbnail_only += 1
+ logging.info(
+ f"Using thumbnail only for {prop_name}, segment {segment_key}"
+ )
+
+ # Run VLM even if we have neither texture nor thumbnail (text-only analysis)
+ if not segment_render_path and not has_thumbnail:
+ segments_text_only_vlm += 1
+ logging.info(
+ f"Running text-only VLM analysis for {segment_key} - no texture or thumbnail available"
+ )
+
+ # Using the make_user_prompt from simready.py
+ from dataset_toolkits.material_objects.vlm_annotations.data_subsets.simready import (
+ make_user_prompt as simready_make_user_prompt,
+ PROMPTS as SIMREADY_PROMPTS,
+ parse_vlm_output as simready_parse_vlm,
+ )
+
+ part1 = simready_make_user_prompt(
+ segment_info["material_type"],
+ segment_info["opacity"],
+ segment_info["density"],
+ segment_info["dynamic_friction"],
+ segment_info["static_friction"],
+ segment_info["restitution"],
+ segment_info["semantic_usage"],
+ has_texture_sphere=segment_render_path is not None,
+ )
+
+ # Store the custom prompt in material_info
+ segment_info["user_prompt"] = part1
+
+ # Debug: Log the prompt type based on texture availability
+ if segment_render_path is not None:
+ logging.info(
+ f"Using prompt WITH texture sphere for {prop_name}, segment {segment_key}"
+ )
+ elif has_thumbnail:
+ logging.info(
+ f"Using prompt WITH thumbnail only for {prop_name}, segment {segment_key}"
+ )
+ else:
+ logging.info(
+ f"Using TEXT-ONLY prompt for {prop_name}, segment {segment_key}"
+ )
+ logging.info(
+ f"PROMPT: {part1[:100]}..."
+ ) # Print just the beginning of the prompt
+
+ segment_info, vlm_analysis_success = (
+ analyze_material_with_retry(
+ segment_key,
+ segment_render_path,
+ segment_info,
+ vlm_model,
+ vlm_processor,
+ thumb_path if has_thumbnail else None,
+ "simready",
+ SIMREADY_PROMPTS,
+ simready_make_user_prompt,
+ simready_parse_vlm,
+ part1,
+ None,
+ )
+ )
+
+ # Add VLM analysis to segment info
+ if vlm_analysis_success:
+ segment_info["vlm_analysis"] = segment_info[
+ "vlm_analysis"
+ ]
+ total_vlm_segments += 1
+ logging.info(
+ f"VLM analysis successful for {segment_key}:"
+ )
+ logging.info(
+ f" Young's modulus: {segment_info.get('youngs_modulus', 'Not extracted')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {segment_info.get('poissons_ratio', 'Not extracted')}"
+ )
+ logging.info(
+ f" Density: {segment_info.get('density', 'Not extracted')}"
+ )
+ else:
+ logging.error(
+ f"VLM analysis failed for {segment_key}"
+ )
+ except Exception as e:
+ import traceback
+
+ logging.error(
+ f"Error during VLM analysis for {segment_key}: {str(e)}"
+ )
+ logging.error(traceback.format_exc())
+
+ total_rendered_segments += 1
+
+ all_results.append(materials_dict)
+ processed_objects.add(object_name)
+ success_count += 1
+
+ # Incremental save after each object if output file is provided
+ if output_file:
+ try:
+ with open(output_file, "w") as f:
+ # Debug save contents
+ logging.info(
+ f"Saving checkpoint with {len(all_results)} objects"
+ )
+
+ # Ensure result types are JSON serializable
+ for idx, item in enumerate(all_results):
+ if "segments" in item:
+ for seg_key, seg_info in item[
+ "segments"
+ ].items():
+ # Remove user_prompt field if it exists
+ if "user_prompt" in seg_info:
+ del seg_info["user_prompt"]
+
+ if "textures" in seg_info and isinstance(
+ seg_info["textures"], dict
+ ):
+ # Convert any non-serializable texture paths to strings
+ serializable_textures = {}
+ for tex_type, tex_path in seg_info[
+ "textures"
+ ].items():
+ serializable_textures[tex_type] = (
+ str(tex_path)
+ )
+ seg_info["textures"] = (
+ serializable_textures
+ )
+
+ json.dump(all_results, f, indent=4, cls=UsdJsonEncoder)
+
+ except Exception as e:
+ logging.error(f"Error saving checkpoint: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ else:
+ logging.warning(f"No segments extracted for {prop_name}")
+ failed_objects.append(prop_name)
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error processing {prop_name}: {str(e)}")
+ logging.error(traceback.format_exc())
+ failed_objects.append(prop_name)
+ else:
+ # For other datasets, obj_info is a tuple of (path, name)
+ usd_file, object_name = obj_info
+
+ try:
+ directory = os.path.dirname(usd_file)
+
+ # Extract material information
+ result = common_extract_materials(usd_file, dataset_type)
+
+ if result:
+ # Ensure file_path is stored in the result to improve future matching
+ if "file_path" not in result or not result["file_path"]:
+ result["file_path"] = usd_file
+
+ # Add to processed tracking
+ processed_file_paths.add(usd_file)
+ processed_files.add(usd_file)
+
+ # Track statistics
+ segments = result.get("segments", {})
+ total_segments += len(segments)
+
+ # Remove object_name and note fields from segments
+ for segment_key, segment_info in segments.items():
+ if "object_name" in segment_info:
+ del segment_info["object_name"]
+ if "note" in segment_info:
+ del segment_info["note"]
+
+ # Count unique materials for this object
+ object_materials = set()
+ for segment_name, segment_info in segments.items():
+ material_name = segment_info.get("material_type", "unknown")
+ unique_materials.add(material_name)
+ object_materials.add(material_name)
+
+ # Record materials per object
+ if len(segments) > 0:
+ materials_per_object[object_name] = len(object_materials)
+
+ # Get thumbnail path if available
+ thumb_path = None
+ # Thumbnails are in .thumbs/256x256 directory
+ thumb_dir = os.path.join(
+ os.path.dirname(usd_file), ".thumbs", "256x256"
+ )
+
+ has_thumbnail = False
+ if os.path.exists(thumb_dir):
+ # Try to find a thumbnail matching the USD filename
+ usd_filename = os.path.basename(usd_file)
+ thumb_candidates = [
+ # Regular thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.png"),
+ # Auto-generated thumbnail
+ os.path.join(thumb_dir, f"{usd_filename}.auto.png"),
+ ]
+
+ for candidate in thumb_candidates:
+ if os.path.exists(candidate):
+ thumb_path = candidate
+ has_thumbnail = True
+ logging.info(f"Found thumbnail: {thumb_path}")
+ break
+
+ # Process VLM for all segments if VLM model is provided
+ os.makedirs("/tmp/vlm", exist_ok=True)
+
+ if vlm_model and len(segments) > 0:
+ for segment_key, segment_info in segments.items():
+ textures = segment_info.get("textures", {})
+
+ # Log texture information for diagnostics
+ logging.info(
+ f"Segment {segment_key} has textures: {list(textures.keys())}"
+ )
+
+ # Check if we have either a normal or roughness texture for rendering
+ has_texture = (
+ "normal" in textures
+ or "roughness" in textures
+ or "diffuse" in textures
+ )
+ if has_texture:
+ # Has texture - render sphere and use with thumbnail
+ segments_with_texture += 1
+ logging.info(
+ f"Rendering texture sphere for {object_name}, segment {segment_key}"
+ )
+
+ # Set up file path for this segment's rendered sphere
+ segment_render_path = f"/tmp/vlm/texture_sphere_{object_name}_{segment_key}.png"
+
+ # Render the textured sphere
+ try:
+ rgb_buffer = render_sphere_with_texture(
+ textures, segment_render_path
+ )
+ logging.info(
+ f"RGB buffer shape: {rgb_buffer.shape}"
+ )
+ except Exception as e:
+ logging.error(
+ f"Error rendering texture for {segment_key}: {str(e)}"
+ )
+ segment_render_path = None
+ else:
+ # No texture - just use thumbnail
+ segments_without_texture += 1
+ segment_render_path = None
+ logging.info(
+ f"No texture for {object_name}, segment {segment_key}. Using thumbnail only."
+ )
+
+ # Always try to process with VLM, even if no texture
+ try:
+ # If we have a thumbnail but no texture, still run VLM with just the thumbnail
+ if not has_texture and has_thumbnail:
+ segments_with_thumbnail_only += 1
+ logging.info(
+ f"Using thumbnail only for {object_name}, segment {segment_key}"
+ )
+
+ # Run VLM even if we have neither texture nor thumbnail (text-only analysis)
+ if not segment_render_path and not has_thumbnail:
+ segments_text_only_vlm += 1
+ logging.info(
+ f"Running text-only VLM analysis for {segment_key} - no texture or thumbnail available"
+ )
+
+ # Set semantic usage to segment name but don't store in segment data
+ semantic_usage = segment_key
+ temp_object_name = object_name
+
+ # Create custom prompt based on texture availability
+ custom_prompt = residential_make_user_prompt(
+ segment_info["material_type"],
+ semantic_usage,
+ temp_object_name,
+ has_texture_sphere=segment_render_path is not None,
+ )
+
+ # Store the custom prompt in material_info but not object_name
+ segment_info["user_prompt"] = custom_prompt
+
+ # Debug: Log the prompt type based on texture availability
+ if segment_render_path is not None:
+ logging.info(
+ f"Using prompt WITH texture sphere for {object_name}, segment {segment_key}"
+ )
+ elif has_thumbnail:
+ logging.info(
+ f"Using prompt WITH thumbnail only for {object_name}, segment {segment_key}"
+ )
+ else:
+ logging.info(
+ f"Using TEXT-ONLY prompt for {object_name}, segment {segment_key}"
+ )
+ logging.info(
+ f"PROMPT: {custom_prompt[:100]}..."
+ ) # Print just the beginning of the prompt
+
+ # Create a temporary segment_info with object_name for VLM but don't save to result
+ temp_segment_info = segment_info.copy()
+ temp_segment_info["semantic_usage"] = semantic_usage
+ temp_segment_info["object_name"] = temp_object_name
+
+ segment_info, vlm_analysis_success = (
+ analyze_material_with_retry(
+ segment_key,
+ segment_render_path,
+ segment_info,
+ vlm_model,
+ vlm_processor,
+ thumb_path if has_thumbnail else None,
+ dataset_type,
+ RESIDENTIAL_PROMPTS,
+ residential_make_user_prompt,
+ parse_vlm_properties,
+ custom_prompt,
+ temp_segment_info,
+ )
+ )
+
+ # Add VLM analysis to segment info
+ if vlm_analysis_success:
+ segment_info["vlm_analysis"] = segment_info[
+ "vlm_analysis"
+ ]
+ total_vlm_segments += 1
+ logging.info(
+ f"VLM analysis successful for {segment_key}:"
+ )
+ logging.info(
+ f" Young's modulus: {segment_info.get('youngs_modulus', 'Not extracted')}"
+ )
+ logging.info(
+ f" Poisson's ratio: {segment_info.get('poissons_ratio', 'Not extracted')}"
+ )
+ logging.info(
+ f" Density: {segment_info.get('density', 'Not extracted')}"
+ )
+ else:
+ logging.error(
+ f"VLM analysis failed for {segment_key}"
+ )
+ except Exception as e:
+ import traceback
+
+ logging.error(
+ f"Error during VLM analysis for {segment_key}: {str(e)}"
+ )
+ logging.error(traceback.format_exc())
+
+ total_rendered_segments += 1
+
+ all_results.append(result) # Add to our local copy of results
+ processed_objects.add(object_name) # Mark as processed
+
+ # Incremental save after each object if output file is provided
+ if output_file:
+ try:
+ with open(output_file, "w") as f:
+ # Debug save contents
+ logging.info(
+ f"Saving checkpoint with {len(all_results)} objects"
+ )
+
+ # Ensure result types are JSON serializable
+ for idx, item in enumerate(all_results):
+ if "segments" in item:
+ for seg_key, seg_info in item[
+ "segments"
+ ].items():
+ # Remove user_prompt field if it exists
+ if "user_prompt" in seg_info:
+ del seg_info["user_prompt"]
+
+ if "textures" in seg_info and isinstance(
+ seg_info["textures"], dict
+ ):
+ # Convert any non-serializable texture paths to strings
+ serializable_textures = {}
+ for tex_type, tex_path in seg_info[
+ "textures"
+ ].items():
+ serializable_textures[tex_type] = (
+ str(tex_path)
+ )
+ seg_info["textures"] = (
+ serializable_textures
+ )
+
+ if dataset_type in ["residential", "vegetation"]:
+ # Try to serialize to a string first to check for issues
+ try:
+ json_str = json.dumps(
+ all_results, cls=UsdJsonEncoder, indent=4
+ )
+ logging.info(
+ f"JSON serialization successful, string length: {len(json_str)}"
+ )
+
+ # Now write to file
+ f.write(json_str)
+ except Exception as json_err:
+ logging.error(
+ f"JSON serialization error: {str(json_err)}"
+ )
+ # Try to identify problematic objects
+ for i, item in enumerate(all_results):
+ try:
+ json.dumps(item, cls=UsdJsonEncoder)
+ except Exception as e:
+ logging.error(
+ f"Error serializing object {i}: {str(e)}"
+ )
+ raise json_err # Re-raise to be caught by outer exception handler
+ else:
+ # Dump to file for other datasets
+ json.dump(
+ all_results, f, indent=4, cls=UsdJsonEncoder
+ )
+
+ except Exception as e:
+ logging.error(f"Error saving checkpoint: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+
+ success_count += 1
+ else:
+ logging.warning(f"No material information extracted for {usd_file}")
+ failed_objects.append(object_name)
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Error processing {usd_file}: {str(e)}")
+ logging.error(traceback.format_exc())
+ failed_objects.append(object_name)
+
+ # Log texture statistics
+ logging.info("Texture Statistics:")
+ logging.info(f" Total segments processed: {total_segments}")
+ logging.info(f" Segments with textures: {segments_with_texture}")
+ logging.info(f" Segments without textures: {segments_without_texture}")
+ logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
+ logging.info(f" Segments with text-only VLM: {segments_text_only_vlm}")
+ logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
+
+ return (
+ all_results,
+ len(objects_to_process),
+ success_count,
+ failed_objects,
+ total_segments,
+ total_rendered_segments,
+ total_vlm_segments,
+ list(unique_materials),
+ materials_per_object,
+ )
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Extract material information from datasets"
+ )
+ parser.add_argument(
+ "--dataset",
+ nargs="+",
+ default=["simready", "residential", "commercial", "vegetation"],
+ help="Name of the dataset to process",
+ )
+ parser.add_argument("--list", action="store_true", help="List all available props")
+ parser.add_argument("--dry-run", action="store_true", help="Dry run the script")
+ parser.add_argument(
+ "--verbose", action="store_true", help="Enable verbose logging to console"
+ )
+ parser.add_argument(
+ "--output", "-o", help="Output JSON file to save combined material information"
+ )
+ parser.add_argument(
+ "--force-reprocess",
+ action="store_true",
+ help="Force reprocessing of objects even if they exist in the output file",
+ )
+ parser.add_argument(
+ "--model",
+ default="qwen",
+ help="Model to use for VLM analysis. Options: 'qwen' (default), 'gemini-2.0-flash', 'gemini-2.5-pro', etc.",
+ )
+ parser.add_argument(
+ "--api-key",
+ help="API key for Gemini models. Required when using Gemini models.",
+ )
+ parser.add_argument(
+ "--limit",
+ type=int,
+ help="Maximum number of objects to process per dataset. Useful for testing.",
+ )
+ args = parser.parse_args()
+
+ # Configure logging
+ if args.verbose:
+ logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+ else:
+ logging.basicConfig(
+ level=logging.WARNING,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+
+ # Validate model and API key
+ if args.model.startswith("gemini") and not args.api_key:
+ logging.error(
+ "API key is required when using Gemini models. Use --api-key to provide it."
+ )
+ sys.exit(1)
+
+ if args.list:
+ for dataset in args.dataset:
+ if dataset == "simready":
+ print(f"Listing SimReady objects")
+ list_simready_objects()
+ elif dataset == "residential":
+ print(f"Listing Residential objects")
+ list_residential_objects()
+ elif dataset == "commercial":
+ print(f"Listing Commercial objects")
+ list_commercial_objects()
+ elif dataset == "vegetation":
+ print(f"Listing Vegetation objects")
+ list_vegetation_objects()
+ sys.exit()
+
+ # Load existing results if output file is specified
+ all_results = []
+ processed_objects = set()
+ processed_file_paths = set()
+ if args.output and not args.force_reprocess:
+ all_results, processed_objects, processed_file_paths = load_existing_results(
+ args.output
+ )
+
+ print("Loading VLM model...")
+ vlm_model, vlm_processor = load_vlm_model(args.model, args.api_key)
+
+ # Initialize statistics tracking dictionaries
+ stats_dict = {
+ "num_objects": {},
+ "success_count": {},
+ "failed_props": {},
+ "total_segments": {},
+ "total_rendered_segments": {},
+ "total_vlm_segments": {},
+ "unique_materials": set(),
+ "materials_per_prop": {},
+ }
+
+ # Process each dataset with the shared all_results and processed_objects
+ for dataset in args.dataset:
+ print(f"Processing {dataset} objects...")
+
+ (
+ all_results,
+ dataset_num_objects,
+ dataset_success_count,
+ dataset_failed_props,
+ dataset_total_segments,
+ dataset_total_rendered_segments,
+ dataset_total_vlm_segments,
+ dataset_unique_materials,
+ dataset_materials_per_prop,
+ ) = process_dataset(
+ dataset,
+ vlm_model,
+ vlm_processor,
+ limit=args.limit if args.limit else (2 if args.dry_run else None),
+ output_file=args.output,
+ dry_run=args.dry_run,
+ force_reprocess=args.force_reprocess,
+ existing_results=all_results,
+ processed_objects=processed_objects,
+ processed_file_paths=processed_file_paths,
+ )
+
+ # Update stats
+ stats_dict["num_objects"][dataset] = dataset_num_objects
+ stats_dict["success_count"][dataset] = dataset_success_count
+ stats_dict["failed_props"][dataset] = dataset_failed_props
+ stats_dict["total_segments"][dataset] = dataset_total_segments
+ stats_dict["total_rendered_segments"][dataset] = dataset_total_rendered_segments
+ stats_dict["total_vlm_segments"][dataset] = dataset_total_vlm_segments
+ stats_dict["unique_materials"].update(dataset_unique_materials)
+ stats_dict["materials_per_prop"][dataset] = dataset_materials_per_prop
+
+ # Final logging
+ if args.verbose:
+ # Calculate total objects processed across all datasets
+ total_objects = sum(stats_dict["num_objects"].values())
+ print(f"Total objects processed: {total_objects}")
+
+ # Print dataset-specific stats
+ processed_datasets = []
+ for dataset in args.dataset:
+ if dataset in stats_dict["num_objects"]:
+ dataset_upper = dataset.upper()
+ processed_datasets.append(dataset_upper)
+ print(f" {dataset_upper}: {stats_dict['num_objects'][dataset]}")
+
+ # Calculate success percentages
+ total_success = sum(stats_dict["success_count"].values())
+ if total_objects > 0:
+ total_success_pct = total_success / total_objects * 100
+ else:
+ total_success_pct = 0
+ print(f"Successfully processed: {total_success} ({total_success_pct:.1f}%)")
+
+ # Print success percentage per dataset
+ for dataset in processed_datasets:
+ dataset_lower = dataset.lower()
+ if (
+ dataset_lower in stats_dict["success_count"]
+ and dataset_lower in stats_dict["num_objects"]
+ and stats_dict["num_objects"][dataset_lower] > 0
+ ):
+ success_pct = (
+ stats_dict["success_count"][dataset_lower]
+ / stats_dict["num_objects"][dataset_lower]
+ * 100
+ )
+ print(f" {dataset}: {success_pct:.1f}%")
+ elif dataset_lower in processed_datasets:
+ print(f" {dataset}: 0.0%")
+
+ # Print segment stats
+ print(f"Total segments: {sum(stats_dict['total_segments'].values())}")
+ print(
+ f"Total rendered segments: {sum(stats_dict['total_rendered_segments'].values())}"
+ )
+ print(f"Total VLM segments: {sum(stats_dict['total_vlm_segments'].values())}")
+ print(f"Unique materials: {len(stats_dict['unique_materials'])}")
+
+ # Cleanup
+ if os.path.exists("/tmp/vlm"):
+ shutil.rmtree("/tmp/vlm")
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/render.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/render.py
new file mode 100644
index 0000000000000000000000000000000000000000..1028b4adfac771402ca750bf6a3e5c6d77700f8f
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/render.py
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+from PIL import Image
+import polyscope as ps
+
+
+def create_sphere(radius=1.0, resolution=64):
+ """
+ Create a sphere mesh with the given radius and resolution.
+ Returns vertices, faces, and UV coordinates.
+ """
+ vertices = []
+ uvs = []
+
+ for i in range(resolution + 1):
+ theta = i * np.pi / resolution
+ for j in range(resolution * 2):
+ phi = j * 2 * np.pi / (resolution * 2)
+
+ x = radius * np.sin(theta) * np.cos(phi)
+ y = radius * np.sin(theta) * np.sin(phi)
+ z = radius * np.cos(theta)
+ vertices.append([x, y, z])
+
+ u = phi / (2 * np.pi)
+ v = 1 - theta / np.pi
+ uvs.append([u, v])
+
+ faces = []
+ for i in range(resolution):
+ for j in range(resolution * 2):
+ next_j = (j + 1) % (resolution * 2)
+
+ p1 = i * (resolution * 2) + j
+ p2 = i * (resolution * 2) + next_j
+ p3 = (i + 1) * (resolution * 2) + j
+ p4 = (i + 1) * (resolution * 2) + next_j
+
+ if i > 0:
+ faces.append([p1, p2, p3])
+ if i < resolution - 1:
+ faces.append([p2, p4, p3])
+
+ return np.array(vertices), np.array(faces), np.array(uvs)
+
+
+def apply_texture_to_vertices(vertices, uvs, texture_path):
+ """
+ Apply texture from file to vertices based on UV coordinates.
+ """
+ # Load the texture image
+ img = Image.open(texture_path)
+ img = img.convert("RGB")
+ texture = np.array(img) / 255.0
+
+ height, width, _ = texture.shape
+ colors = np.zeros((len(vertices), 3))
+
+ for i, uv in enumerate(uvs):
+ x = int(uv[0] * (width - 1))
+ y = int((1 - uv[1]) * (height - 1))
+ colors[i] = texture[y, x]
+
+ return colors
+
+
+def apply_normal_mapped_colors(vertices, faces, uvs, normal_path, base_colors):
+ """
+ Apply normal mapping effect to the base colors.
+ """
+ # Load the normal map
+ img = Image.open(normal_path)
+ img = img.convert("RGB")
+ normal_texture = np.array(img) / 255.0
+
+ height, width, _ = normal_texture.shape
+
+ # Convert normal map from [0,1] range to [-1,1] range
+ normals_from_map = normal_texture * 2.0 - 1.0
+
+ # Simple lighting setup (directional light from above-right)
+ light_dir = np.array([0.5, 0.5, 1.0])
+ light_dir = light_dir / np.linalg.norm(light_dir)
+
+ ambient = 0.3 # Ambient light intensity
+ diffuse = 0.7 # Diffuse light intensity
+
+ # Apply normal-based lighting
+ adjusted_colors = base_colors.copy()
+ for i, uv in enumerate(uvs):
+ x = int(uv[0] * (width - 1))
+ y = int((1 - uv[1]) * (height - 1))
+
+ # Extract normal from texture
+ normal = normals_from_map[y, x]
+ normal = normal / np.linalg.norm(normal)
+
+ # Calculate lighting factor using normal
+ diffuse_factor = max(0, np.dot(normal, light_dir))
+ light_factor = ambient + diffuse * diffuse_factor
+
+ # Apply lighting to base color
+ adjusted_colors[i] = base_colors[i] * light_factor
+
+ # Ensure values stay in [0,1] range
+ adjusted_colors[i] = np.clip(adjusted_colors[i], 0, 1)
+
+ return adjusted_colors
+
+
+def render_sphere_with_texture(textures_dict, output_path=None):
+ """
+ Render a sphere with the given textures.
+ Returns the RGB numpy array of the rendered image.
+
+ Args:
+ textures_dict (dict): Dictionary of texture paths by type
+ output_path (str, optional): Path to save the output image if desired
+
+ Returns:
+ numpy.ndarray: RGB image array of the rendered sphere
+ """
+ # Create sphere mesh
+ vertices, faces, uvs = create_sphere(radius=1.0, resolution=64)
+
+ # Register the sphere mesh with Polyscope
+ ps_mesh = ps.register_surface_mesh("sphere", vertices, faces)
+
+ try:
+ # Apply textures
+ if "albedo" in textures_dict:
+ base_colors = apply_texture_to_vertices(
+ vertices, uvs, textures_dict["albedo"]
+ )
+ final_colors = base_colors.copy()
+
+ # Apply normal map if available
+ if "normal" in textures_dict:
+ final_colors = apply_normal_mapped_colors(
+ vertices, faces, uvs, textures_dict["normal"], final_colors
+ )
+
+ # Add the final material color to the mesh
+ ps_mesh.add_color_quantity(
+ "material", final_colors, defined_on="vertices", enabled=True
+ )
+ else:
+ # Fallback to a simple colored material
+ ps_mesh.add_color_quantity(
+ "default",
+ np.ones((len(vertices), 3)) * np.array([0.7, 0.7, 0.7]),
+ defined_on="vertices",
+ enabled=True,
+ )
+
+ except Exception as e:
+ print(f"Error processing textures: {str(e)}")
+ ps_mesh.add_color_quantity(
+ "default",
+ np.ones((len(vertices), 3)) * np.array([0.7, 0.7, 0.7]),
+ defined_on="vertices",
+ enabled=True,
+ )
+
+ # Set up camera and rendering settings
+ ps.reset_camera_to_home_view()
+ ps.set_ground_plane_mode("none")
+ ps.set_up_dir("z_up")
+ ps.set_front_dir("neg_y_front")
+
+ ps_mesh.set_smooth_shade(True)
+
+ # Capture screenshot as buffer (RGBA)
+ rgba_buffer = ps.screenshot_to_buffer(transparent_bg=False)
+
+ # Convert RGBA to RGB
+ h, w, _ = rgba_buffer.shape
+ rgb_buffer = np.zeros((h, w, 3), dtype=np.uint8)
+
+ # Just take the RGB channels from RGBA
+ rgb_buffer = rgba_buffer[:, :, :3]
+
+ # Save to file if output path is provided
+ if output_path:
+ # Save the RGB buffer to file
+ Image.fromarray(rgb_buffer).save(output_path)
+
+ # Clean up (remove the mesh from polyscope)
+ ps.remove_surface_mesh("sphere")
+
+ return rgb_buffer
+
+
+def process_rgba_to_rgb(image_path):
+ """
+ Convert RGBA image to RGB, making fully transparent pixels white.
+ """
+ try:
+ # Open the image
+ img = Image.open(image_path)
+
+ # Check if the image has an alpha channel
+ if img.mode == "RGBA":
+ # Create a white background
+ background = Image.new("RGB", img.size, (255, 255, 255))
+
+ # Paste the image on the background using itself as mask
+ background.paste(img, mask=img.split()[3])
+
+ # Save the result to the same path
+ background.save(image_path.replace(".png", "_rgb.png"))
+ return image_path.replace(".png", "_rgb.png")
+ elif img.mode != "RGB":
+ # Convert any other mode to RGB
+ img = img.convert("RGB")
+ img.save(image_path.replace(".png", "_rgb.png"))
+ return image_path.replace(".png", "_rgb.png")
+ else:
+ # Already RGB, return the original path
+ return image_path
+
+ except Exception as e:
+ print(f"Error processing image {image_path}: {e}")
+ return None
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/utils.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..1eb58b948fd9b2f7d49328f575c59e235b4d1dd1
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/utils.py
@@ -0,0 +1,185 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import numpy as np
+import os
+import csv
+import difflib
+import logging
+from qwen_vl_utils import process_vision_info
+from typing import Tuple, List, Dict
+from pathlib import Path
+
+from dataset_toolkits.datasets.simready import (
+ get_asset_class_mapping as get_simready_asset_class_mapping,
+)
+
+# Resolve path to material_ranges.csv relative to this file's location
+UTILS_FILE_PATH = Path(__file__).resolve()
+# utils.py is in dataset_toolkits/material_objects/vlm_annotations/utils/
+# WORKSPACE_ROOT is 4 levels up from 'utils'
+WORKSPACE_ROOT = UTILS_FILE_PATH.parents[4]
+MATERIAL_RANGES_CSV_ABSOLUTE = (
+ WORKSPACE_ROOT / "datasets" / "latent_space" / "material_ranges_old.csv"
+)
+
+# Keep the old constant for now if anything else relies on it by name, but new logic uses _ABSOLUTE
+MATERIAL_RANGES_CSV = "datasets/latent_space/material_ranges_old.csv"
+
+BASE_DIR = "datasets/raw"
+
+SIMREADY_BASE_DIR = os.path.join(BASE_DIR, "simready")
+SIMREADY_PROPS_DIR = os.path.join(SIMREADY_BASE_DIR, "common_assets", "props")
+SIMREADY_MATERIALS_DIR = os.path.join(SIMREADY_BASE_DIR, "materials", "physics")
+SIMREADY_ASSET_INFO_PATH = os.path.join(SIMREADY_BASE_DIR, "asset_info.json")
+SIMREADY_ASSET_CLASS_MAPPING = get_simready_asset_class_mapping(
+ SIMREADY_ASSET_INFO_PATH
+)
+
+RESIDENTIAL_BASE_DIR = os.path.join(BASE_DIR, "residential")
+
+COMMERCIAL_BASE_DIR = os.path.join(BASE_DIR, "commercial")
+
+VEGETATION_BASE_DIR = os.path.join(BASE_DIR, "vegetation")
+
+
+def set_seed(seed: int = 42) -> None:
+ torch.random.manual_seed(seed)
+ np.random.seed(seed)
+ torch.cuda.manual_seed(seed)
+
+
+def load_material_ranges(csv_path=None):
+ if csv_path is None:
+ csv_path = MATERIAL_RANGES_CSV_ABSOLUTE
+
+ # Ensure csv_path is a string for os.path.exists if it's a Path object
+ csv_path_str = str(csv_path)
+
+ if not os.path.exists(csv_path_str):
+ logging.warning(f"Material ranges CSV file not found at {csv_path_str}")
+ return []
+
+ material_ranges = []
+
+ with open(csv_path_str, "r", newline="", encoding="utf-8") as f:
+ # Read the first line to get/clean the header
+ header_line = f.readline().strip()
+ if header_line.startswith('"') and header_line.endswith('"'):
+ header_line = header_line[
+ 1:-1
+ ] # Remove surrounding quotes from the whole line
+
+ fieldnames = [name.strip() for name in header_line.split(",")]
+
+ # Use the cleaned fieldnames for DictReader
+ # The rest of the file (f) is now positioned after the header line
+ reader = csv.DictReader(f, fieldnames=fieldnames)
+ for i, row in enumerate(reader):
+ try:
+ # Normalize keys for easier usage
+ material_ranges.append(
+ {
+ "name": row["Material Name"].strip(),
+ "youngs": row["Young's Modulus Range [GPa]"].strip(),
+ "poisson": row["Poisson's Ratio Range"].strip(),
+ "density": row["Density Range"].strip(),
+ }
+ )
+ except KeyError as e:
+ logging.error(
+ f"KeyError processing row {i+1} in {csv_path_str}: {e}. Row data: {row}. Expected headers: {fieldnames}"
+ )
+ # Optionally skip this row or raise the error
+ continue
+ except Exception as e:
+ logging.error(
+ f"Error processing row {i+1} in {csv_path_str}: {e}. Row data: {row}"
+ )
+ continue
+ return material_ranges
+
+
+def find_reference_materials(material_db, material_query, max_matches=3):
+ """Return a list of reference materials whose names fuzzy-match the query."""
+
+ material_query = (material_query or "").lower()
+ if not material_query:
+ return []
+
+ names = [m["name"] for m in material_db]
+
+ close_names = difflib.get_close_matches(
+ material_query, names, n=max_matches, cutoff=0.4
+ )
+
+ refs = [m for m in material_db if m["name"] in close_names]
+ if not refs:
+ refs = [m for m in material_db if material_query in m["name"].lower()][
+ :max_matches
+ ]
+ return refs
+
+
+def round_float_to_2dp(value):
+ if isinstance(value, float):
+ return round(value, 2)
+ return value
+
+
+def parse_numerical_range_str(range_str: str) -> Tuple[float, float]:
+ """
+ Parses a string that can be a single number or a range in brackets.
+ Examples: "2.5", "[2.5]", "[2.5, 3.5]"
+ Returns a tuple (min_val, max_val).
+ For a single number, min_val will be equal to max_val.
+ """
+ cleaned_str = str(range_str).strip() # Ensure input is string
+ if cleaned_str.startswith("[") and cleaned_str.endswith("]"):
+ cleaned_str = cleaned_str[1:-1].strip()
+
+ if not cleaned_str: # Handle empty string or "[]"
+ raise ValueError(
+ f"Input string for range parsing is empty or invalid: '{range_str}'"
+ )
+
+ parts = [part.strip() for part in cleaned_str.split(",")]
+
+ try:
+ if len(parts) == 1:
+ if not parts[0]: # Handles "[]" which becomes ""
+ raise ValueError(
+ f"Input string for range parsing is empty or invalid after stripping brackets: '{range_str}'"
+ )
+ val = float(parts[0])
+ return val, val
+ elif len(parts) == 2:
+ low = float(parts[0])
+ high = float(parts[1])
+ # Optional: ensure low <= high.
+ # if low > high:
+ # # Consider logging a warning or raising an error if min > max
+ # return high, low # Or handle as error
+ return low, high
+ else:
+ raise ValueError(
+ f"Invalid range string format: '{range_str}'. Expected 1 or 2 parts after splitting by comma, got {len(parts)}."
+ )
+ except ValueError as e:
+ # Re-raise with more context
+ raise ValueError(
+ f"Error parsing numerical range from string '{range_str}': {e}"
+ )
diff --git a/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/vlm.py b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/vlm.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a2b8796df77616f3953a514e0ad5e1a4a4604a6
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/vlm_annotations/utils/vlm.py
@@ -0,0 +1,600 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os
+import re
+import torch
+import base64
+
+from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration
+from qwen_vl_utils import process_vision_info
+from openai import OpenAI
+
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ find_reference_materials,
+ load_material_ranges,
+ parse_numerical_range_str,
+)
+
+SYSTEM_PROMPT = """
+You are a materials science expert specializing in analyzing material properties from visual appearances and physical data. Your task is to provide precise numerical estimates for Young's modulus, Poisson's ratio, and density based on the images and context provided.
+
+Important Context: The material segment you are analyzing may be an internal component or structure that is not visible from the outside of the object. For example:
+- Internal support structures, frames, or reinforcements
+- Hidden layers or core materials
+- Components enclosed within the outer shell
+- Structural elements that are only visible when the object is disassembled
+
+When analyzing:
+- Consider that the material might be completely hidden from external view
+- Use the semantic usage and material type hints to infer properties of internal components
+- Internal structural components often have different properties than visible surfaces
+- For example, a soft exterior might hide a rigid internal frame
+
+Critical Instruction: You MUST provide numerical estimates for ALL materials, even organic, biological, or unusual materials like leaves, feathers, or paper.
+- For organic materials, estimate properties based on similar natural materials with known values
+- For leaves, consider them as thin plant fiber composites with values similar to paper or dried plant fibers
+- Never respond with "N/A" or any non-numeric value in your property estimates
+
+When analyzing materials, use step-by-step reasoning:
+1. First identify the likely material class and subtype based on visual appearance (if visible) or contextual clues (if internal)
+2. Consider how texture, color, and reflectivity inform your understanding of the material (when visible)
+3. Incorporate the provided physical properties and contextual usage information
+4. For each mechanical property, reason through how the visual and physical attributes lead to your estimate
+5. Consider how the material compares to reference materials with known properties
+6. If the material appears to be internal/hidden, use the object type and usage context to make informed estimates
+
+Important Formatting Requirements:
+- Young's modulus must be provided in scientific notation followed by "Pa" (e.g., 2.0e11 Pa)
+- Poisson's ratio must be a simple decimal between 0.0 and 0.5 with no units (e.g., 0.34)
+- Density must be provided in kg/m^3 (e.g., 7800 kg/m^3)
+- Each property must be on its own line with exactly the label shown in the examples
+- Do not include explanatory text or parenthetical notes after the values
+- ALWAYS provide numerical values, never text like "N/A" or "unknown"
+"""
+
+MATERIAL_RANGES = load_material_ranges()
+
+
+def encode_image(image_path):
+ """Encode image to base64 for Gemini API."""
+ with open(image_path, "rb") as image_file:
+ return base64.b64encode(image_file.read()).decode("utf-8")
+
+
+def parse_vlm_properties(output_text):
+ """
+ Parses VLM output text to extract analysis and mechanical properties.
+ Uses multiple strategies to handle different formats and potential parsing issues.
+
+ Args:
+ output_text (str): Raw output text from the VLM
+
+ Returns:
+ dict: Dictionary with extracted analysis and properties
+ """
+ analysis = ""
+ youngs_modulus = None
+ poissons_ratio = None
+ density = None
+
+ analysis_lines = []
+ capturing_analysis = False
+
+ for line in output_text.split("\n"):
+ line = line.strip()
+ if line.lower().startswith("analysis:"):
+ capturing_analysis = True
+ analysis_lines.append(line.split(":", 1)[1].strip())
+ elif capturing_analysis and (
+ line.lower().startswith("young")
+ or line.lower().startswith("poisson")
+ or line.lower().startswith("density")
+ ):
+ capturing_analysis = False
+ elif capturing_analysis and line:
+ analysis_lines.append(line)
+
+ analysis = "\n".join(analysis_lines)
+
+ try:
+
+ modulus_line = ""
+ for line in output_text.split("\n"):
+ if line.lower().startswith("young"):
+ modulus_line = line
+ break
+
+ if modulus_line:
+
+ numeric_matches = re.findall(r"([0-9][0-9.eE+\-]*)", modulus_line)
+ if numeric_matches:
+ value_str = numeric_matches[0].replace(",", "").strip()
+ youngs_modulus = float(value_str)
+ logging.info(
+ f"Successfully extracted Young's modulus: {youngs_modulus} from line '{modulus_line}'"
+ )
+
+ if youngs_modulus < 1000 and "g" in modulus_line.lower():
+ youngs_modulus *= 1.0e9
+ logging.info(
+ f"Converted Young's modulus from GPa to Pa: {youngs_modulus}"
+ )
+ except Exception as e:
+ logging.error(f"First Young's modulus extraction approach failed: {e}")
+
+ if youngs_modulus is None:
+ try:
+ modulus_pattern = re.compile(
+ r"young'?s\s+modulus\s*[:\-]?\s*([0-9][0-9.eE+\-]*)", re.IGNORECASE
+ )
+ modulus_match = modulus_pattern.search(output_text)
+ if modulus_match:
+ value_str = modulus_match.group(1).replace(",", "")
+ youngs_modulus = float(value_str)
+
+ if (
+ youngs_modulus < 1000
+ and "g" in output_text.lower().split("young")[1].split("\n")[0]
+ ):
+ youngs_modulus *= 1.0e9
+
+ logging.info(
+ f"Second Young's modulus extraction approach succeeded: {youngs_modulus}"
+ )
+ except Exception as e:
+ logging.error(f"Second Young's modulus extraction approach failed: {e}")
+
+ try:
+
+ ratio_line = ""
+ for line in output_text.split("\n"):
+ if line.lower().startswith("poisson"):
+ ratio_line = line
+ break
+
+ if ratio_line:
+
+ numeric_matches = re.findall(r"([0-9][0-9.eE+\-]*)", ratio_line)
+ if numeric_matches:
+ value_str = numeric_matches[0].replace(",", "").strip()
+ poissons_ratio = float(value_str)
+ logging.info(
+ f"Successfully extracted Poisson's ratio: {poissons_ratio}"
+ )
+
+ if poissons_ratio < 0 or poissons_ratio > 0.5:
+ logging.warning(
+ f"Poisson's ratio out of normal range: {poissons_ratio}"
+ )
+ except Exception as e:
+ logging.error(f"First Poisson's ratio extraction approach failed: {e}")
+
+ if poissons_ratio is None:
+ try:
+ ratio_pattern = re.compile(
+ r"poisson'?s\s+ratio\s*[:\-]?\s*([0-9][0-9.eE+\-]*)", re.IGNORECASE
+ )
+ ratio_match = ratio_pattern.search(output_text)
+ if ratio_match:
+ value_str = ratio_match.group(1).replace(",", "")
+ poissons_ratio = float(value_str)
+ logging.info(
+ f"Second Poisson's ratio extraction approach succeeded: {poissons_ratio}"
+ )
+ except Exception as e:
+ logging.error(f"Second Poisson's ratio extraction approach failed: {e}")
+
+ try:
+
+ density_line = ""
+ for line in output_text.split("\n"):
+ if line.lower().startswith("density"):
+ density_line = line
+ break
+
+ if density_line:
+
+ cleaned_line = density_line.replace("kg/mยณ", "kg/m3").replace(
+ "kg/m^3", "kg/m3"
+ )
+
+ numeric_matches = re.findall(r"([0-9][0-9,.eE+\-]*)", cleaned_line)
+ if numeric_matches:
+ value_str = numeric_matches[0].replace(",", "").strip()
+ density = float(value_str)
+ logging.info(f"Successfully extracted density: {density}")
+ except Exception as e:
+ logging.error(f"First density extraction approach failed: {e}")
+
+ if density is None:
+ try:
+
+ density_pattern = re.compile(
+ r"density\s*[:\-]?\s*([0-9][0-9,.eE+\-]*)", re.IGNORECASE
+ )
+ density_match = density_pattern.search(output_text)
+ if density_match:
+ value_str = density_match.group(1).replace(",", "")
+ density = float(value_str)
+ logging.info(f"Second density extraction approach succeeded: {density}")
+ except Exception as e:
+ logging.error(f"Second density extraction approach failed: {e}")
+
+ if density is None:
+ try:
+ simple_pattern = re.compile(
+ r"density.*?(\d+(?:\.\d+)?)", re.IGNORECASE | re.DOTALL
+ )
+ simple_match = simple_pattern.search(output_text)
+ if simple_match:
+ density = float(simple_match.group(1))
+ logging.info(f"Last resort density extraction succeeded: {density}")
+ except Exception as e:
+ logging.error(f"Last resort density extraction failed: {e}")
+
+ result = {
+ "vlm_analysis": analysis,
+ "youngs_modulus": youngs_modulus,
+ "poissons_ratio": poissons_ratio,
+ "density": density,
+ "raw_vlm_output": output_text,
+ }
+
+ missing_properties = []
+ if youngs_modulus is None:
+ missing_properties.append("Young's modulus")
+ if poissons_ratio is None:
+ missing_properties.append("Poisson's ratio")
+ if density is None:
+ missing_properties.append("density")
+
+ if missing_properties:
+ logging.warning(
+ f"Missing properties in VLM output: {', '.join(missing_properties)}"
+ )
+ else:
+ logging.info(
+ f"All properties extracted: Young's modulus={youngs_modulus}, Poisson's ratio={poissons_ratio}, Density={density}"
+ )
+
+ return result
+
+
+def load_vlm_model(model_type="qwen", api_key=None):
+ logging.info(f"Loading VLM model: {model_type}")
+
+ if model_type == "qwen":
+ try:
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
+ "Qwen/Qwen2.5-VL-72B-Instruct",
+ torch_dtype=torch.bfloat16,
+ device_map="auto",
+ attn_implementation="flash_attention_2",
+ )
+
+ min_pixels = 256 * 28 * 28
+ max_pixels = 1280 * 28 * 28
+ processor = AutoProcessor.from_pretrained(
+ "Qwen/Qwen2.5-VL-72B-Instruct",
+ min_pixels=min_pixels,
+ max_pixels=max_pixels,
+ )
+
+ logging.info("Qwen VLM model loaded successfully")
+ return model, processor
+ except Exception as e:
+ logging.error(f"Failed to load Qwen VLM model: {str(e)}")
+ return None, None
+
+ elif model_type.startswith("gemini"):
+ if not api_key:
+ logging.error("API key is required for Gemini models")
+ return None, None
+
+ try:
+
+ client = OpenAI(
+ api_key=api_key,
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
+ )
+
+ logging.info(f"Gemini client created successfully for model: {model_type}")
+
+ return client, model_type
+ except Exception as e:
+ logging.error(f"Failed to create Gemini client: {str(e)}")
+ return None, None
+
+ elif model_type == "llama-3.2-90b-vision" or model_type == "llama-vision":
+
+ if not api_key:
+
+ api_key = os.getenv("NVIDIA_API_KEY")
+ if not api_key:
+ logging.error(
+ "NVIDIA API key is required for llama-3.2-90b-vision model"
+ )
+ return None, None
+
+ try:
+
+ client = OpenAI(
+ api_key=api_key,
+ base_url="https://integrate.api.nvidia.com/v1",
+ )
+
+ logging.info("NVIDIA llama-3.2-90b-vision client created successfully")
+
+ return client, "nvdev/meta/llama-3.2-90b-vision-instruct"
+ except Exception as e:
+ logging.error(f"Failed to create NVIDIA llama vision client: {str(e)}")
+ return None, None
+
+ else:
+ logging.error(f"Unsupported model type: {model_type}")
+ return None, None
+
+
+def analyze_material_with_vlm(
+ image_path,
+ material_info,
+ model,
+ processor,
+ thumbnail_path,
+ dataset_name,
+ PROMPTS,
+ make_user_prompt,
+ parse_vlm_output,
+):
+ if dataset_name == "simready":
+ material_type = material_info.get("material_type", "unknown")
+ opacity = material_info.get("opacity", "opaque")
+ density = material_info.get("density", None)
+ dynamic_friction = material_info.get("dynamic_friction", None)
+ static_friction = material_info.get("static_friction", None)
+ restitution = material_info.get("restitution", None)
+ semantic_usage = material_info.get("semantic_usage", "")
+
+ if "user_prompt" in material_info:
+ part1 = material_info["user_prompt"]
+ else:
+
+ part1 = make_user_prompt(
+ material_type,
+ opacity,
+ density,
+ dynamic_friction,
+ static_friction,
+ restitution,
+ semantic_usage,
+ has_texture_sphere=image_path is not None,
+ )
+ elif (
+ dataset_name == "residential"
+ or dataset_name == "commercial"
+ or dataset_name == "vegetation"
+ ):
+ material_type = material_info.get("material_type", "unknown")
+ semantic_usage = material_info.get("semantic_usage", "")
+ object_name = material_info.get("object_name", "")
+
+ if "user_prompt" in material_info:
+ part1 = material_info["user_prompt"]
+ else:
+
+ part1 = make_user_prompt(
+ material_type,
+ semantic_usage,
+ object_name,
+ has_texture_sphere=image_path is not None,
+ )
+
+ reference_info = ""
+ ref_materials = find_reference_materials(MATERIAL_RANGES, material_type)
+ if ref_materials:
+ reference_info += "\nAdditional reference material property ranges to help you make accurate estimations:\n"
+ for rm in ref_materials:
+ try:
+ y_min, y_max = parse_numerical_range_str(rm["youngs"])
+ p_min, p_max = parse_numerical_range_str(rm["poisson"])
+ d_min, d_max = parse_numerical_range_str(rm["density"])
+
+ youngs_range_str = f"{y_min}" if y_min == y_max else f"{y_min}-{y_max}"
+ poisson_range_str = f"{p_min}" if p_min == p_max else f"{p_min}-{p_max}"
+ density_range_str = f"{d_min}" if d_min == d_max else f"{d_min}-{d_max}"
+
+ except ValueError as e:
+ logging.warning(
+ f"Could not parse range for material {rm.get('name', 'Unknown')}: {e}. Skipping this reference material."
+ )
+ continue
+
+ reference_info += (
+ f" - {rm['name']}: Young's modulus range {youngs_range_str} GPa, "
+ f"Poisson's ratio range {poisson_range_str}, Density range {density_range_str} kg/m^3\n"
+ )
+
+ prompt_text = f"""
+{part1}
+
+{reference_info}
+
+{PROMPTS["few_shot_examples"]}
+
+{PROMPTS["query_prompt"]}
+"""
+
+ if isinstance(processor, str) and processor.startswith("gemini"):
+
+ try:
+
+ message_content = []
+
+ message_content.append(
+ {"type": "text", "text": SYSTEM_PROMPT + "\n\n" + prompt_text}
+ )
+
+ if thumbnail_path and os.path.exists(thumbnail_path):
+ base64_thumbnail = encode_image(thumbnail_path)
+ message_content.append(
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": f"data:image/png;base64,{base64_thumbnail}"
+ },
+ }
+ )
+ logging.info(f"Added thumbnail image to Gemini request")
+
+ if image_path and os.path.exists(image_path):
+ base64_texture = encode_image(image_path)
+ message_content.append(
+ {
+ "type": "image_url",
+ "image_url": {"url": f"data:image/png;base64,{base64_texture}"},
+ }
+ )
+ logging.info(f"Added texture sphere image to Gemini request")
+
+ response = model.chat.completions.create(
+ model=processor,
+ messages=[{"role": "user", "content": message_content}],
+ max_tokens=4096,
+ temperature=0.7,
+ )
+
+ output_text = response.choices[0].message.content
+
+ return parse_vlm_properties(output_text)
+
+ except Exception as e:
+ logging.error(f"Error in Gemini VLM analysis: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return {"error": str(e), "raw_vlm_output": "Error generating response"}
+
+ elif isinstance(processor, str) and (
+ processor.startswith("nvdev") or "llama-3.2-90b-vision" in processor
+ ):
+
+ try:
+
+ message_content = []
+
+ message_content.append(
+ {"type": "text", "text": SYSTEM_PROMPT + "\n\n" + prompt_text}
+ )
+
+ if thumbnail_path and os.path.exists(thumbnail_path):
+ base64_thumbnail = encode_image(thumbnail_path)
+ message_content.append(
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": f"data:image/png;base64,{base64_thumbnail}"
+ },
+ }
+ )
+ logging.info(f"Added thumbnail image to NVIDIA llama vision request")
+
+ if image_path and os.path.exists(image_path):
+ base64_texture = encode_image(image_path)
+ message_content.append(
+ {
+ "type": "image_url",
+ "image_url": {"url": f"data:image/png;base64,{base64_texture}"},
+ }
+ )
+ logging.info(
+ f"Added texture sphere image to NVIDIA llama vision request"
+ )
+
+ response = model.chat.completions.create(
+ model=processor,
+ messages=[{"role": "user", "content": message_content}],
+ max_tokens=4096,
+ temperature=0.5,
+ )
+
+ output_text = response.choices[0].message.content
+
+ return parse_vlm_properties(output_text)
+
+ except Exception as e:
+ logging.error(f"Error in NVIDIA llama vision VLM analysis: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return {"error": str(e), "raw_vlm_output": "Error generating response"}
+
+ else:
+
+ user_content = []
+
+ if thumbnail_path:
+ thumb_uri = f"file://{os.path.abspath(thumbnail_path)}"
+ logging.info(f"Using thumbnail image: {thumb_uri}")
+ user_content.append({"type": "image", "image": thumb_uri})
+
+ if image_path:
+
+ absolute_image_path = os.path.abspath(image_path)
+ file_uri = f"file://{absolute_image_path}"
+ user_content.append({"type": "image", "image": file_uri})
+
+ user_content.append({"type": "text", "text": prompt_text})
+
+ messages = [
+ {"role": "system", "content": SYSTEM_PROMPT},
+ {"role": "user", "content": user_content},
+ ]
+
+ try:
+ text = processor.apply_chat_template(
+ messages, tokenize=False, add_generation_prompt=True
+ )
+
+ image_inputs, video_inputs = process_vision_info(messages)
+ inputs = processor(
+ text=[text],
+ images=image_inputs,
+ videos=video_inputs,
+ return_tensors="pt",
+ )
+
+ inputs = inputs.to(model.device)
+
+ generated_ids = model.generate(**inputs, max_new_tokens=4096)
+ generated_ids_trimmed = [
+ out_ids[len(in_ids) :]
+ for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
+ ]
+ output_text = processor.batch_decode(
+ generated_ids_trimmed,
+ skip_special_tokens=True,
+ clean_up_tokenization_spaces=False,
+ )[0]
+
+ return parse_vlm_properties(output_text)
+
+ except Exception as e:
+ logging.error(f"Error in VLM analysis: {str(e)}")
+ import traceback
+
+ logging.error(traceback.format_exc())
+ return {"error": str(e), "raw_vlm_output": "Error generating response"}
diff --git a/deps/vomp/dataset_toolkits/material_objects/voxelize.py b/deps/vomp/dataset_toolkits/material_objects/voxelize.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d0c5f612882156eeea35d8d66bf0de91469524b
--- /dev/null
+++ b/deps/vomp/dataset_toolkits/material_objects/voxelize.py
@@ -0,0 +1,778 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import copy
+import sys
+import importlib
+import argparse
+import pandas as pd
+import json
+from easydict import EasyDict as edict
+from functools import partial
+import numpy as np
+import open3d as o3d
+import utils3d
+import numpy as np
+import tempfile
+import shutil
+from pxr import Usd, UsdGeom, Gf, UsdShade
+import re
+import trimesh
+
+sys.path.append(os.path.dirname(__file__))
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
+ extract_materials_from_usd,
+)
+
+# For fuzzy fallback using material ranges CSV
+from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
+ load_material_ranges,
+ find_reference_materials,
+ parse_numerical_range_str,
+)
+
+# SimReady helper to parse material names into opacity/material_type/semantic_usage
+from dataset_toolkits.material_objects.vlm_annotations.data_subsets.simready import (
+ parse_material_name,
+)
+
+DEFAULT_MATERIAL_PROPS = {
+ "youngs_modulus": 1e6, # Default Young's modulus in Pa
+ "poisson_ratio": 0.3, # Default Poisson ratio
+ "density": 1000.0, # Default density in kg/m^3
+ "friction": 0.5, # Default friction coefficient
+ "dynamic_friction": 0.5, # Default dynamic friction
+ "static_friction": 0.5, # Default static friction
+ "restitution": 0.3, # Default restitution
+}
+
+MATERIAL_DATASET_PATH = "datasets/raw/material_annotations.json"
+
+
+def load_material_dataset(material_dataset_path):
+ """
+ Load the material dataset JSON file.
+
+ Args:
+ material_dataset_path (str): Path to the material dataset JSON
+
+ Returns:
+ list: List of material objects from the JSON file
+ """
+ try:
+ with open(material_dataset_path, "r") as f:
+ material_data = json.load(f)
+
+ # Return the raw list from the JSON file
+ return material_data
+ except Exception as e:
+ print(f"Error loading material dataset: {str(e)}")
+ return []
+
+
+def voxelize_mesh(
+ vertices, faces, voxel_size=1 / 64, center_scale=None, max_voxels=None
+):
+ """
+ Voxelize a mesh represented by vertices and faces using volumetric voxelization.
+
+ Args:
+ vertices (numpy.ndarray): Array of vertices
+ faces (numpy.ndarray): Array of faces
+ voxel_size (float): Size of each voxel
+ center_scale (tuple): Optional center and scale for normalization
+ max_voxels (int): Maximum number of voxels to return (will subsample if exceeded)
+
+ Returns:
+ tuple: (voxel_centers, voxel_grid) - center coordinates of voxels and Trimesh voxel grid
+ """
+ # Create a Trimesh mesh
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
+
+ # Normalize the mesh to [-0.5, 0.5] range
+ vertices = mesh.vertices.copy()
+
+ if center_scale is None:
+ vertices_min = np.min(vertices, axis=0)
+ vertices_max = np.max(vertices, axis=0)
+ center = (vertices_min + vertices_max) / 2
+ scale = np.max(vertices_max - vertices_min)
+ else:
+ center, scale = center_scale
+
+ vertices = (vertices - center) / scale
+ vertices = np.clip(vertices, -0.5 + 1e-6, 0.5 - 1e-6)
+
+ # Update mesh with normalized vertices
+ mesh.vertices = vertices
+
+ # Create volumetric voxel grid using Trimesh
+ # Calculate pitch (voxel size) and use voxelized method with fill for volumetric voxelization
+ voxel_grid = mesh.voxelized(pitch=voxel_size).fill()
+
+ # Get voxel centers from the filled voxel grid
+ voxel_centers = voxel_grid.points
+
+ # Subsample if we have too many voxels
+ if max_voxels is not None and len(voxel_centers) > max_voxels:
+ print(f"Subsampling voxels: {len(voxel_centers):,} -> {max_voxels:,}")
+ # Use random sampling to maintain spatial distribution
+ np.random.seed(42) # For reproducibility
+ indices = np.random.choice(len(voxel_centers), max_voxels, replace=False)
+ voxel_centers = voxel_centers[indices]
+
+ return voxel_centers, voxel_grid
+
+
+def extract_and_voxelize_segments(
+ usd_file_path, sha256, output_dir, material_dataset_path, max_voxels=None
+):
+ """
+ Extract each segment from a 'zag_middle' USD model, voxelize them,
+ and save combined voxel data with material properties.
+
+ Args:
+ usd_file_path (str): Path to the USD file
+ output_dir (str): Directory to save extracted segments
+ material_dataset_path (str): Path to the material dataset JSON
+ max_voxels (int): Maximum number of voxels per asset (will subsample if exceeded)
+
+ Returns:
+ bool: Success or failure
+ """
+ if not os.path.exists(usd_file_path):
+ print(f"Error: USD file not found at {usd_file_path}")
+ return False
+
+ # Load material dataset
+ material_lookup = load_material_dataset(material_dataset_path)
+ if not material_lookup:
+ print("Warning: Material dataset could not be loaded or is empty.")
+
+ # Load reference material ranges once for fuzzy fallback
+ material_db = load_material_ranges()
+
+ # Extract the object name from the file path - use basename without extensions
+ object_name = os.path.splitext(os.path.basename(usd_file_path))[0]
+
+ # Remove common suffixes
+ object_name = (
+ object_name.replace("_inst_base", "").replace("_inst", "").replace("_base", "")
+ )
+
+ # Normalize name: strip "sm_" prefix (SimReady naming) and trailing variant indices like "_01"
+ original_lower = object_name.lower()
+
+ # Prepare list of candidate names starting with the full original
+ object_name_candidates = [original_lower]
+
+ # If name starts with 'sm_', also add the version without it
+ if original_lower.startswith("sm_"):
+ stripped = original_lower[3:]
+ object_name_candidates.append(stripped)
+ else:
+ stripped = original_lower
+
+ # Variantless (remove numeric suffix) from both versions
+ variantless = re.sub(r"_[0-9]{1,2}$", "", stripped)
+ if variantless not in object_name_candidates:
+ object_name_candidates.append(variantless)
+
+ # Store for later use in fuzzy fallback
+ object_name_lower = original_lower
+
+ # DEBUG: Print the extracted object name
+ # print(f"DEBUG: Extracted object name: {object_name}")
+ # print(f"DEBUG: USD file path: {usd_file_path}")
+
+ try:
+ # Load USD stage
+ # Do NOT modify the path here โ keep the original _inst_base if present
+
+ # We no longer rely on extract_materials_from_usd for segment names; instead we
+ # directly construct segment keys from mesh paths and match them against the
+ # JSON dataset. This avoids the extra USD-parsing pass used in VLM annotation.
+
+ # DEBUG: Print all segments found
+ # (Removed: dependency on material_result)
+
+ stage = Usd.Stage.Open(usd_file_path)
+ if not stage:
+ print(f"Error: Failed to open USD stage from {usd_file_path}")
+ return False
+
+ default_prim = stage.GetDefaultPrim()
+ if not default_prim:
+ print("DEBUG: No default prim found, using root")
+ default_prim = stage.GetPrimAtPath("/")
+
+ # Find all mesh prims
+ mesh_prims = []
+ for prim in Usd.PrimRange(default_prim):
+ if prim.GetTypeName() == "Mesh":
+ mesh_prims.append(prim)
+ # DEBUG: Print each mesh found
+ # print(f"DEBUG: Found mesh: {prim.GetPath()}")
+
+ if not mesh_prims:
+ print(f"Error: No meshes found in {usd_file_path}")
+ return False
+
+ # Compute global bounding box across all meshes to preserve relative positions
+ all_vertices = []
+ for prim in mesh_prims:
+ mesh_tmp = UsdGeom.Mesh(prim)
+ pts_attr = mesh_tmp.GetPointsAttr()
+ if pts_attr and pts_attr.Get():
+ points_local = pts_attr.Get()
+ # Get world transform for this mesh
+ xformable = UsdGeom.Xformable(prim)
+ world_transform = xformable.ComputeLocalToWorldTransform(
+ Usd.TimeCode.Default()
+ )
+ # Transform points to world space
+ verts_tmp = []
+ for p in points_local:
+ p_world = world_transform.Transform(Gf.Vec3d(p[0], p[1], p[2]))
+ verts_tmp.append(
+ [float(p_world[0]), float(p_world[1]), float(p_world[2])]
+ )
+ verts_tmp = np.array(verts_tmp, dtype=np.float32)
+ all_vertices.append(verts_tmp)
+
+ if len(all_vertices) == 0:
+ print("DEBUG: No vertices found in any mesh")
+ return False
+
+ all_vertices_concat = np.vstack(all_vertices)
+ global_min = np.min(all_vertices_concat, axis=0)
+ global_max = np.max(all_vertices_concat, axis=0)
+ global_center = (global_min + global_max) / 2
+ global_scale = np.max(global_max - global_min)
+ global_center_scale = (global_center, global_scale)
+
+ # Store voxels and properties for all segments
+ all_voxel_centers = []
+ all_material_props = []
+ all_segment_ids = []
+
+ # Process each mesh as a potential segment
+ for mesh_prim in mesh_prims:
+ mesh = UsdGeom.Mesh(mesh_prim)
+ mesh_name = mesh_prim.GetName()
+
+ # Attempt to get bound material name for this mesh
+ material_name = None
+ try:
+ binding_api = UsdShade.MaterialBindingAPI(mesh_prim)
+ direct_binding = binding_api.GetDirectBinding()
+ if direct_binding and direct_binding.GetMaterial():
+ material_name = direct_binding.GetMaterial().GetPath().name
+ else:
+ # Try collection bindings
+ for col_binding in binding_api.GetCollectionBindings():
+ mat = col_binding.GetMaterial()
+ if mat:
+ material_name = mat.GetPath().name
+ break
+ except Exception:
+ material_name = None
+
+ # Get the full path for more accurate segment identification
+ full_path = str(mesh_prim.GetPath())
+ path_parts = full_path.split("/")
+ # Skip the root and "World" prefixes
+ parent_parts = [p for p in path_parts[2:-1] if p]
+
+ # Construct segment key that matches the naming convention in common.py
+ segment_key = mesh_name
+ if parent_parts:
+ segment_key = "_".join(parent_parts + [mesh_name])
+
+ # Lowercase version for case-insensitive lookups
+ segment_key_lower = segment_key.lower()
+
+ # print(f"DEBUG: Processing segment key: {segment_key}")
+
+ # Get vertices
+ points_attr = mesh.GetPointsAttr()
+ if not points_attr or not points_attr.Get():
+ print(f"DEBUG: No vertices in mesh {segment_key}, skipping")
+ continue
+
+ points_local = points_attr.Get()
+ # Get world transform for this mesh
+ xformable = UsdGeom.Xformable(mesh_prim)
+ world_transform = xformable.ComputeLocalToWorldTransform(
+ Usd.TimeCode.Default()
+ )
+ # Transform points to world space
+ vertices = []
+ for p in points_local:
+ p_world = world_transform.Transform(Gf.Vec3d(p[0], p[1], p[2]))
+ vertices.append(
+ [float(p_world[0]), float(p_world[1]), float(p_world[2])]
+ )
+ vertices = np.array(vertices, dtype=np.float32)
+
+ # Get face information
+ face_vertex_counts = mesh.GetFaceVertexCountsAttr().Get()
+ face_vertex_indices = mesh.GetFaceVertexIndicesAttr().Get()
+
+ if not face_vertex_counts or not face_vertex_indices:
+ print(f"DEBUG: No faces in mesh {segment_key}, skipping")
+ continue
+
+ # Extract faces
+ faces = []
+ idx = 0
+ for count in face_vertex_counts:
+ if count == 3:
+ # Triangle
+ face = [
+ face_vertex_indices[idx],
+ face_vertex_indices[idx + 1],
+ face_vertex_indices[idx + 2],
+ ]
+ faces.append(face)
+ elif count == 4:
+ # Triangulate quad into two triangles
+ face1 = [
+ face_vertex_indices[idx],
+ face_vertex_indices[idx + 1],
+ face_vertex_indices[idx + 2],
+ ]
+ face2 = [
+ face_vertex_indices[idx],
+ face_vertex_indices[idx + 2],
+ face_vertex_indices[idx + 3],
+ ]
+ faces.append(face1)
+ faces.append(face2)
+ idx += count
+
+ faces = np.array(faces, dtype=np.int32)
+
+ # We no longer consult VLM-extracted segments โ rely purely on JSON file
+
+ # Check for this segment key in the material_lookup
+ material_props = DEFAULT_MATERIAL_PROPS.copy()
+ segment_found = False
+ ref_applied = False
+
+ # Loop through objects to find one with matching object_name (case-insensitive)
+ for obj in material_lookup:
+ if str(obj.get("object_name", "")).lower() in object_name_candidates:
+ obj_segments = obj.get("segments", {})
+
+ # Deterministic keys to try (mirrors extract_materials_from_usd logic)
+ candidate_keys = [segment_key, mesh_name, f"{mesh_name}_whole"]
+ if material_name:
+ candidate_keys.append(material_name)
+
+ # Build SimReady-style key if possible
+ try:
+ opacity, mat_type, semantic_usage = parse_material_name(
+ material_name
+ )
+ if semantic_usage:
+ simready_key = f"opaque__{mat_type}__{semantic_usage}"
+ candidate_keys.append(simready_key)
+ # Default fallback naming
+ if (
+ "default__" not in material_name.lower()
+ and mat_type
+ and object_name
+ ):
+ candidate_keys.append(
+ f"default__{mat_type}__{object_name}"
+ )
+ except Exception:
+ pass
+
+ segment_data = None
+ for key in candidate_keys:
+ # Try exact match first
+ if key in obj_segments:
+ segment_data = obj_segments[key]
+ segment_found = True
+ break
+ # Case-insensitive match
+ for seg_name, seg_info in obj_segments.items():
+ if seg_name.lower() == key.lower():
+ segment_data = seg_info
+ segment_found = True
+ break
+ if segment_found:
+ break
+
+ # After candidate key checks, attempt prefix fallbacks if still not found
+ if not segment_found:
+ # Case-insensitive prefix check between segment_key / mesh_name and JSON keys
+ for seg_name, seg_info in obj_segments.items():
+ seg_low = seg_name.lower()
+ if (
+ seg_low.startswith(segment_key_lower)
+ or segment_key_lower.startswith(seg_low)
+ or seg_low.startswith(mesh_name.lower())
+ or (
+ material_name
+ and seg_low.startswith(material_name.lower())
+ )
+ ):
+ segment_data = seg_info
+ segment_found = True
+ break
+
+ # Additional prefix2 fallback (first two tokens)
+ if not segment_found:
+ mesh_tokens = mesh_name.split("_")
+ if len(mesh_tokens) >= 2:
+ prefix2 = "_".join(mesh_tokens[:2]).lower()
+ for seg_name, seg_info in obj_segments.items():
+ if seg_name.lower().startswith(prefix2):
+ segment_data = seg_info
+ segment_found = True
+ break
+
+ if segment_found and segment_data is not None:
+ # Extract material properties directly
+ material_props["density"] = float(
+ segment_data.get(
+ "density", DEFAULT_MATERIAL_PROPS["density"]
+ )
+ )
+ material_props["youngs_modulus"] = float(
+ segment_data.get(
+ "youngs_modulus",
+ DEFAULT_MATERIAL_PROPS["youngs_modulus"],
+ )
+ )
+ material_props["poisson_ratio"] = float(
+ segment_data.get(
+ "poissons_ratio",
+ DEFAULT_MATERIAL_PROPS["poisson_ratio"],
+ )
+ )
+
+ # Optional properties (ignored if missing)
+ else:
+ # If no segment found, keep default material properties
+ pass
+
+ # Break out after processing the matching object
+ break
+
+ if not segment_found:
+ print(f"DEBUG: Using DEFAULT material properties (no segment match)")
+ print("Object name: ", object_name)
+ print("Segment key: ", segment_key)
+
+ # Fuzzy fallback: use bound material name to fetch approximate values
+ if material_name:
+ refs = find_reference_materials(
+ material_db, material_name, max_matches=1
+ )
+ if refs:
+ ref = refs[0]
+ try:
+ y_min, y_max = parse_numerical_range_str(ref.get("youngs"))
+ material_props["youngs_modulus"] = (
+ (y_min + y_max) / 2 if y_max else y_min
+ ) * 1e9 # convert GPa to Pa
+ except Exception:
+ pass
+ try:
+ p_min, p_max = parse_numerical_range_str(ref.get("poisson"))
+ material_props["poisson_ratio"] = (
+ (p_min + p_max) / 2 if p_max else p_min
+ )
+ except Exception:
+ pass
+ try:
+ d_min, d_max = parse_numerical_range_str(ref.get("density"))
+ material_props["density"] = (
+ (d_min + d_max) / 2 if d_max else d_min
+ )
+ except Exception:
+ pass
+ ref_applied = True
+ print(
+ f"DEBUG: Fuzzy fallback applied using reference material '{ref.get('name')}' for material '{material_name}'"
+ )
+
+ # If still not applied, try fuzzy match on object name and segment key
+ if not ref_applied:
+ extra_candidates = [segment_key_lower, mesh_name.lower()]
+ for cand in extra_candidates:
+ # strict containment search only (no fuzzy)
+ refs = [
+ m
+ for m in material_db
+ if cand in m["name"].lower() or m["name"].lower() in cand
+ ][:1]
+ if refs:
+ ref = refs[0]
+ try:
+ y_min, y_max = parse_numerical_range_str(
+ ref.get("youngs")
+ )
+ material_props["youngs_modulus"] = (
+ (y_min + y_max) / 2 if y_max else y_min
+ ) * 1e9
+ except Exception:
+ pass
+ try:
+ p_min, p_max = parse_numerical_range_str(
+ ref.get("poisson")
+ )
+ material_props["poisson_ratio"] = (
+ (p_min + p_max) / 2 if p_max else p_min
+ )
+ except Exception:
+ pass
+ try:
+ d_min, d_max = parse_numerical_range_str(
+ ref.get("density")
+ )
+ material_props["density"] = (
+ (d_min + d_max) / 2 if d_max else d_min
+ )
+ except Exception:
+ pass
+ ref_applied = True
+ print(
+ f"DEBUG: Fuzzy fallback applied using reference material '{ref.get('name')}' for candidate '{cand}'"
+ )
+ break
+
+ # If we still have neither a direct match nor a reference fallback, skip this mesh
+ if not segment_found and not ref_applied:
+ print(f"DEBUG: Skipping mesh {segment_key} โ no material data found")
+ continue
+
+ # Voxelize the mesh
+ voxel_centers, _ = voxelize_mesh(
+ vertices, faces, center_scale=global_center_scale, max_voxels=max_voxels
+ )
+ # print(f"DEBUG: Segment {segment_key} generated {len(voxel_centers)} voxels")
+
+ # Add voxels and properties to the combined arrays
+ if len(voxel_centers) > 0:
+ all_voxel_centers.append(voxel_centers)
+
+ # Create array of material properties for all voxels in this segment
+ segment_props = np.tile(
+ [
+ material_props["youngs_modulus"],
+ material_props["poisson_ratio"],
+ material_props["density"],
+ ],
+ (len(voxel_centers), 1),
+ )
+ all_material_props.append(segment_props)
+
+ # Use segment_key directly as the segment ID
+ all_segment_ids.extend([segment_key] * len(voxel_centers))
+
+ # Combine all voxels and properties
+ if all_voxel_centers:
+ combined_voxel_centers = np.vstack(all_voxel_centers)
+ combined_material_props = np.vstack(all_material_props)
+
+ # Apply global max_voxels limit if specified and exceeded
+ if max_voxels is not None and len(combined_voxel_centers) > max_voxels:
+ print(
+ f"Global subsampling: {len(combined_voxel_centers):,} -> {max_voxels:,} voxels"
+ )
+ np.random.seed(42) # For reproducibility
+ indices = np.random.choice(
+ len(combined_voxel_centers), max_voxels, replace=False
+ )
+ combined_voxel_centers = combined_voxel_centers[indices]
+ combined_material_props = combined_material_props[indices]
+ all_segment_ids = [all_segment_ids[i] for i in indices]
+
+ # print(f"DEBUG: Combined model has {len(combined_voxel_centers)} voxels")
+
+ ply_output_path = os.path.join(output_dir, "voxels", f"{sha256}.ply")
+ save_ply(ply_output_path, combined_voxel_centers)
+ # print(f"DEBUG: Saved voxel centers to {ply_output_path}")
+
+ ply_positions = utils3d.io.read_ply(ply_output_path)[0]
+ ply_positions = ply_positions.astype(np.float32)
+ indices = ((ply_positions + 0.5) * 64).astype(np.int64)
+ indices = np.clip(indices, 0, 63)
+ discretized_positions = indices.astype(np.float32) / 64.0 - 0.5
+ # print(f"DEBUG: Materials using same count as PLY: {len(discretized_positions)} voxels")
+ # Save voxel centers and material properties as a single NPZ file
+ voxel_data = np.zeros(
+ len(discretized_positions),
+ dtype=[
+ ("x", " str:
+ sha256 = hashlib.sha256()
+ # Read the file from the path
+ with open(file, "rb") as f:
+ # Update the hash with the file content
+ for byte_block in iter(lambda: f.read(4096), b""):
+ sha256.update(byte_block)
+ return sha256.hexdigest()
+
+
+# ===============LOW DISCREPANCY SEQUENCES================
+
+PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]
+
+
+def radical_inverse(base, n):
+ val = 0
+ inv_base = 1.0 / base
+ inv_base_n = inv_base
+ while n > 0:
+ digit = n % base
+ val += digit * inv_base_n
+ n //= base
+ inv_base_n *= inv_base
+ return val
+
+
+def halton_sequence(dim, n):
+ return [radical_inverse(PRIMES[dim], n) for dim in range(dim)]
+
+
+def hammersley_sequence(dim, n, num_samples):
+ return [n / num_samples] + halton_sequence(dim - 1, n)
+
+
+def sphere_hammersley_sequence(n, num_samples, offset=(0, 0)):
+ u, v = hammersley_sequence(2, n, num_samples)
+ u += offset[0] / num_samples
+ v += offset[1]
+ u = 2 * u if u < 0.25 else 2 / 3 * u + 1 / 3
+ theta = np.arccos(1 - 2 * u) - np.pi / 2
+ phi = v * 2 * np.pi
+ return [phi, theta]
diff --git a/deps/vomp/datasets/.gitkeep b/deps/vomp/datasets/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deps/vomp/environment.yml b/deps/vomp/environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f4ff8447b9bf2d34ffc797379d8757a6b81df40a
--- /dev/null
+++ b/deps/vomp/environment.yml
@@ -0,0 +1,503 @@
+name: vomp
+channels:
+ - conda-forge
+ - defaults
+ - https://repo.anaconda.com/pkgs/main
+ - https://repo.anaconda.com/pkgs/r
+dependencies:
+ - _libgcc_mutex=0.1=main
+ - _openmp_mutex=5.1=1_gnu
+ - blas=2.131=openblas
+ - blas-devel=3.9.0=31_h1ea3ea9_openblas
+ - bzip2=1.0.8=h5eee18b_6
+ - ca-certificates=2025.6.15=hbd8a1cb_0
+ - certifi=2025.6.15=py310h06a4308_0
+ - debugpy=1.8.11=py310h6a678d5_0
+ - freetype=2.10.4=h0708190_1
+ - gmp=6.3.0=h6a678d5_0
+ - ipydatawidgets=4.3.2=pyhc268e32_0
+ - ipykernel=6.29.5=pyh3099207_0
+ - jupyter_client=8.6.3=pyhd8ed1ab_1
+ - jupyter_core=5.8.1=pyh31011fe_0
+ - ld_impl_linux-64=2.40=h12ee557_0
+ - libblas=3.9.0=31_h59b9bed_openblas
+ - libcblas=3.9.0=31_he106b2a_openblas
+ - libffi=3.4.4=h6a678d5_1
+ - libgcc-ng=11.2.0=h1234567_1
+ - libgfortran-ng=11.2.0=h00389a5_1
+ - libgfortran5=11.2.0=h1234567_1
+ - libgomp=11.2.0=h1234567_1
+ - liblapack=3.9.0=31_h7ac8fdf_openblas
+ - liblapacke=3.9.0=31_he2f377e_openblas
+ - libopenblas=0.3.29=ha39b09d_0
+ - libpng=1.6.37=h21135ba_2
+ - libpython-static=3.10.16=h6a678d5_1
+ - libstdcxx-ng=13.2.0=hc0a3c3a_7
+ - libuuid=1.41.5=h5eee18b_0
+ - matplotlib-base=3.5.2=py310h5701ce4_0
+ - meshplot=0.4.0=pyhd3deb0d_2
+ - metis=5.1.0=hf484d3e_4
+ - mpfr=4.2.1=h5eee18b_0
+ - ncurses=6.4=h6a678d5_0
+ - nomkl=3.0=0
+ - openblas=0.3.29=h06a4308_0
+ - openblas-devel=0.3.29=ha39b09d_0
+ - openssl=3.0.16=h5eee18b_0
+ - pip=25.0=py310h06a4308_0
+ - polyfempy=0.5.2=py310hed0c3e6_7
+ - python=3.10.16=he870216_1
+ - python_abi=3.10=2_cp310
+ - pythreejs=2.4.2=pyhbbac1ac_1
+ - readline=8.2=h5eee18b_0
+ - setuptools=75.8.0=py310h06a4308_0
+ - sqlite=3.45.3=h5eee18b_0
+ - suitesparse=5.10.1=he2db622_2
+ - tbb=2022.0.0=hdb19cb5_0
+ - tk=8.6.14=h39e8969_0
+ - wheel=0.45.1=py310h06a4308_0
+ - xz=5.6.4=h5eee18b_1
+ - zlib=1.2.13=h5eee18b_1
+ - pip:
+ - absl-py==2.2.2
+ - accelerate==1.6.0
+ - addict==2.4.0
+ - aiofiles==23.2.1
+ - aiohappyeyeballs==2.6.1
+ - aiohttp==3.11.18
+ - aiosignal==1.3.2
+ - alembic==1.15.2
+ - altair==5.5.0
+ - annotated-types==0.7.0
+ - antlr4-python3-runtime==4.9.3
+ - anyio==4.9.0
+ - appdirs==1.4.4
+ - argon2-cffi==25.1.0
+ - argon2-cffi-bindings==21.2.0
+ - arrgh==1.0.0
+ - arrow==1.3.0
+ - asttokens==3.0.0
+ - async-lru==2.0.5
+ - async-timeout==5.0.1
+ - attrs==25.3.0
+ - awscli==1.40.44
+ - babel==2.17.0
+ - banal==1.0.6
+ - beautifulsoup4==4.13.4
+ - bidict==0.23.1
+ - black==25.1.0
+ - bleach==6.2.0
+ - blinker==1.9.0
+ - bokeh==3.7.3
+ - botocore==1.38.45
+ - bpy==4.0.0
+ - cachetools==6.1.0
+ - ccimport==0.4.4
+ - cffi==1.17.1
+ - cfgv==3.4.0
+ - charset-normalizer==3.4.1
+ - click==8.1.8
+ - cloudpickle==3.1.1
+ - colorama==0.4.6
+ - coloredlogs==15.0.1
+ - colorlog==6.9.0
+ - comet-ml==3.49.11
+ - comm==0.2.2
+ - configargparse==1.7
+ - configobj==5.0.9
+ - contourpy==1.3.1
+ - cryptography==45.0.4
+ - cuda-bindings==12.9.0
+ - cuda-python==12.6.2.post1
+ - cumm-cu118==0.7.11
+ - cumm-cu120==0.4.11
+ - cupy==13.3.0
+ - cycler==0.12.1
+ - cython==3.1.2
+ - dash==3.0.2
+ - dataclasses-json==0.6.7
+ - dataset==1.6.2
+ - datasets==3.5.1
+ - decorator==5.2.1
+ - deepspeed==0.14.4
+ - defusedxml==0.7.1
+ - deprecated==1.2.18
+ - descartes==1.1.0
+ - detectron2==0.6
+ - diff-gaussian-rasterization==0.0.0
+ - diff-gaussian-rasterization-contrastive-f==0.0.0
+ - diff-gaussian-rasterization-depth==0.0.0
+ - diff-plane-rasterization==0.0.0
+ - diffoctreerast==0.0.0
+ - dill==0.4.0
+ - distlib==0.3.9
+ - distro==1.9.0
+ - docstring-parser==0.16
+ - docutils==0.19
+ - dulwich==0.23.0
+ - easydict==1.13
+ - einops==0.8.1
+ - embreex==2.17.7.post6
+ - entrypoints==0.4
+ - et-xmlfile==2.0.0
+ - everett==3.1.0
+ - exceptiongroup==1.2.2
+ - executing==2.2.0
+ - fastapi==0.115.12
+ - fastjsonschema==2.21.1
+ - fastrlock==0.8.3
+ - ffmpy==0.5.0
+ - filelock==3.13.1
+ - fire==0.7.0
+ - flash-attn==2.7.4.post1
+ - flask==3.0.3
+ - flatbuffers==25.2.10
+ - fonttools==4.56.0
+ - fpsample==0.3.3
+ - fqdn==1.5.1
+ - frozenlist==1.6.0
+ - fsspec==2024.6.1
+ - ftfy==6.3.1
+ - fvcore==0.1.5.post20221221
+ - gdown==5.2.0
+ - gitdb==4.0.12
+ - gitpython==3.1.44
+ - glcontext==3.0.0
+ - gmsh==4.13.1
+ - gputil==1.4.0
+ - gradio==4.27.0
+ - gradio-box-promptable-image==0.0.1
+ - gradio-client==0.15.1
+ - gradio-litmodel3d==0.0.1
+ - greenlet==3.2.1
+ - grpcio==1.71.0
+ - gsplat==1.4.0
+ - h11==0.14.0
+ - h5py==3.14.0
+ - hdbscan==0.8.40
+ - hjson==3.1.0
+ - httpcore==1.0.7
+ - httpx==0.28.1
+ - huggingface-hub==0.30.1
+ - humanfriendly==10.0
+ - hydra-core==1.3.2
+ - identify==2.6.12
+ - idna==3.10
+ - igraph==0.11.8
+ - imageio==2.37.0
+ - imageio-ffmpeg==0.6.0
+ - imgui==2.0.0
+ - importlib-metadata==8.6.1
+ - importlib-resources==6.5.2
+ - iniconfig==2.1.0
+ - iopath==0.1.10
+ - ipycanvas==0.13.3
+ - ipyevents==2.0.2
+ - ipython==8.34.0
+ - ipywidgets==8.1.5
+ - isoduration==20.11.0
+ - itsdangerous==2.2.0
+ - jaxtyping==0.3.2
+ - jedi==0.19.2
+ - jinja2==3.1.4
+ - jiter==0.10.0
+ - jmespath==1.0.1
+ - joblib==1.4.2
+ - json5==0.12.0
+ - jsonpointer==3.0.0
+ - jsonschema==4.23.0
+ - jsonschema-specifications==2024.10.1
+ - jupyter==1.1.1
+ - jupyter-client==7.4.9
+ - jupyter-console==6.6.3
+ - jupyter-core==5.7.2
+ - jupyter-events==0.12.0
+ - jupyter-lsp==2.2.5
+ - jupyter-server==2.16.0
+ - jupyter-server-terminals==0.5.3
+ - jupyterlab==4.4.3
+ - jupyterlab-pygments==0.3.0
+ - jupyterlab-server==2.27.3
+ - jupyterlab-widgets==3.0.13
+ - k3d==2.16.1
+ - kiwisolver==1.4.8
+ - lark==1.2.2
+ - lazy-loader==0.4
+ - lightning-utilities==0.14.3
+ - llvmlite==0.44.0
+ - loguru==0.7.3
+ - lpips==0.1.4
+ - lxml==6.0.0
+ - mako==1.3.10
+ - manifold3d==3.1.1
+ - mapbox-earcut==1.0.3
+ - markdown==3.7
+ - markdown-it-py==3.0.0
+ - markupsafe==2.1.5
+ - marshmallow==3.26.1
+ - matplotlib==3.10.1
+ - matplotlib-inline==0.1.7
+ - mdurl==0.1.2
+ - mediapy==1.2.4
+ - meshio==5.3.5
+ - mistune==3.1.3
+ - moderngl==5.12.0
+ - mpmath==1.3.0
+ - msgpack==1.1.0
+ - msgpack-numpy==0.4.8
+ - msgspec==0.19.0
+ - multidict==6.4.3
+ - multiprocess==0.70.18
+ - mypy==1.16.1
+ - mypy-extensions==1.0.0
+ - narwhals==1.33.0
+ - nbclient==0.10.2
+ - nbconvert==7.16.6
+ - nbformat==5.10.4
+ - nerfacc==0.5.2
+ - nerfstudio==1.1.5
+ - nest-asyncio==1.6.0
+ - networkx==3.3
+ - ninja==1.11.1.4
+ - nltk==3.3
+ - nodeenv==1.9.1
+ - notebook==7.4.3
+ - notebook-shim==0.2.4
+ - npzviewer==0.2.0
+ - numba==0.61.0
+ - numpy==1.26.4
+ - nuscenes-devkit==1.1.9
+ - nvdiffrast==0.3.3
+ - nvidia-cublas-cu11==11.11.3.6
+ - nvidia-cublas-cu12==12.6.4.1
+ - nvidia-cuda-cupti-cu11==11.8.87
+ - nvidia-cuda-cupti-cu12==12.6.80
+ - nvidia-cuda-nvrtc-cu11==11.8.89
+ - nvidia-cuda-nvrtc-cu12==12.6.77
+ - nvidia-cuda-runtime-cu11==11.8.89
+ - nvidia-cuda-runtime-cu12==12.6.77
+ - nvidia-cudnn-cu11==9.1.0.70
+ - nvidia-cudnn-cu12==9.5.1.17
+ - nvidia-cufft-cu11==10.9.0.58
+ - nvidia-cufft-cu12==11.3.0.4
+ - nvidia-cufile-cu12==1.11.1.6
+ - nvidia-curand-cu11==10.3.0.86
+ - nvidia-curand-cu12==10.3.7.77
+ - nvidia-cusolver-cu11==11.4.1.48
+ - nvidia-cusolver-cu12==11.7.1.2
+ - nvidia-cusparse-cu11==11.7.5.86
+ - nvidia-cusparse-cu12==12.5.4.2
+ - nvidia-cusparselt-cu12==0.6.3
+ - nvidia-ml-py==12.575.51
+ - nvidia-nccl-cu11==2.20.5
+ - nvidia-nccl-cu12==2.26.2
+ - nvidia-nvjitlink-cu12==12.6.85
+ - nvidia-nvtx-cu11==11.8.86
+ - nvidia-nvtx-cu12==12.6.77
+ - objaverse==0.1.7
+ - omegaconf==2.3.0
+ - omniverse-kit==106.5.0.162521
+ - onnxruntime==1.21.0
+ - open-clip-torch==2.32.0
+ - open3d==0.19.0
+ - openai==1.82.0
+ - opencv-python==4.8.0.76
+ - opencv-python-headless==4.10.0.84
+ - openpyxl==3.1.2
+ - orjson==3.10.16
+ - outcome==1.3.0.post0
+ - overrides==7.7.0
+ - packaging==24.2
+ - pandas==2.2.3
+ - pandocfilters==1.5.1
+ - parameterized==0.9.0
+ - parso==0.8.4
+ - pathos==0.3.4
+ - pathspec==0.12.1
+ - pccm==0.4.16
+ - pexpect==4.9.0
+ - pillow==10.4.0
+ - platformdirs==4.3.7
+ - plotly==6.0.1
+ - pluggy==1.6.0
+ - plyfile==1.1
+ - polyscope==2.4.0
+ - pooch==1.8.2
+ - portalocker==3.1.1
+ - pox==0.3.6
+ - ppft==1.7.7
+ - pre-commit==4.2.0
+ - prometheus-client==0.22.1
+ - prompt-toolkit==3.0.50
+ - propcache==0.3.1
+ - protobuf==3.20.3
+ - psutil==7.0.0
+ - ptyprocess==0.7.0
+ - pure-eval==0.2.3
+ - py-cpuinfo==9.0.0
+ - pyarrow==19.0.1
+ - pyasn1==0.6.1
+ - pybind11==2.13.6
+ - pycocotools==2.0.8
+ - pycollada==0.9
+ - pycparser==2.22
+ - pydantic==2.11.2
+ - pydantic-core==2.33.1
+ - pydub==0.25.1
+ - pyglet==1.5.31
+ - pygltflib==1.16.3
+ - pygments==2.19.1
+ - pyliblzfse==0.4.1
+ - pymatting==1.1.13
+ - pymeshfix==0.17.0
+ - pymeshlab==2023.12.post3
+ - pyngrok==7.2.11
+ - pynvml==12.0.0
+ - pyopengl==3.1.9
+ - pyparsing==3.2.3
+ - pyqt5==5.15.11
+ - pyqt5-qt5==5.15.16
+ - pyqt5-sip==12.17.0
+ - pyquaternion==0.9.9
+ - pyside6==6.9.0
+ - pyside6-addons==6.9.0
+ - pyside6-essentials==6.9.0
+ - pysocks==1.7.1
+ - pytest==8.3.5
+ - python-box==6.1.0
+ - python-dateutil==2.9.0.post0
+ - python-engineio==4.12.2
+ - python-json-logger==3.3.0
+ - python-multipart==0.0.20
+ - python-socketio==5.13.0
+ - pytorch-msssim==1.0.0
+ - pytorch3d==0.7.8
+ - pytorchvideo==0.1.5
+ - pytz==2025.2
+ - pyuipc==0.9.0
+ - pyvista==0.44.2
+ - pywsd==1.2.4
+ - pyyaml==6.0.2
+ - pyzmq==26.3.0
+ - rawpy==0.25.0
+ - referencing==0.36.2
+ - regex==2024.11.6
+ - rembg==2.0.65
+ - requests==2.32.3
+ - requests-toolbelt==1.0.0
+ - retrying==1.3.4
+ - rfc3339-validator==0.1.4
+ - rfc3986-validator==0.1.1
+ - rich==14.0.0
+ - rpds-py==0.24.0
+ - rsa==4.7.2
+ - rtree==1.4.0
+ - ruff==0.11.3
+ - s2wrapper==0.1
+ - s3transfer==0.13.0
+ - safetensors==0.5.3
+ - sam-2==1.0
+ - scikit-image==0.25.2
+ - scikit-learn==1.6.1
+ - scipy==1.15.2
+ - scooby==0.10.0
+ - seaborn==0.13.2
+ - segment-anything==1.0
+ - segment-anything-hq==0.3
+ - selenium==4.33.0
+ - semantic-version==2.10.0
+ - send2trash==1.8.3
+ - sentry-sdk==2.32.0
+ - setproctitle==1.3.6
+ - shapely==2.1.1
+ - shellingham==1.5.4
+ - shiboken6==6.9.0
+ - shtab==1.7.2
+ - simple-knn==0.0.0
+ - simple-parsing==0.1.7
+ - simple-websocket==1.1.0
+ - simplejson==3.20.1
+ - six==1.17.0
+ - smmap==5.0.2
+ - sniffio==1.3.1
+ - sortedcontainers==2.4.0
+ - soupsieve==2.7
+ - spconv-cu118==2.3.8
+ - spconv-cu120==2.3.6
+ - splines==0.3.0
+ - sqlalchemy==1.4.54
+ - stack-data==0.6.3
+ - starlette==0.46.1
+ - svg-path==6.3
+ - sympy==1.14.0
+ - tabulate==0.9.0
+ - tensorboard==2.19.0
+ - tensorboard-data-server==0.7.2
+ - tensorly==0.9.0
+ - termcolor==3.0.1
+ - terminado==0.18.1
+ - texttable==1.7.0
+ - threadpoolctl==3.6.0
+ - tifffile==2025.3.30
+ - timm==0.6.7
+ - tinycss2==1.4.0
+ - tinycudann==2.0
+ - tokenize-rt==6.1.0
+ - tokenizers==0.21.1
+ - tomli==2.2.1
+ - tomlkit==0.12.0
+ - torch==2.4.0+cu118
+ - torch-fidelity==0.3.0
+ - torchaudio==2.4.0+cu118
+ - torchmetrics==1.7.3
+ - torchvision==0.19.0+cu118
+ - tornado==6.4.2
+ - tqdm==4.67.1
+ - traitlets==5.14.3
+ - traittypes==0.2.1
+ - transformers==4.50.3
+ - trimesh==4.6.6
+ - trio==0.30.0
+ - trio-websocket==0.12.2
+ - triton==3.0.0
+ - typeguard==4.4.2
+ - typer==0.15.2
+ - types-python-dateutil==2.9.0.20250516
+ - typing-extensions==4.12.2
+ - typing-inspect==0.9.0
+ - typing-inspection==0.4.0
+ - tyro==0.9.22
+ - tzdata==2025.2
+ - uri-template==1.3.0
+ - urllib3==2.4.0
+ - usd-core==25.2.post1
+ - utils3d==0.0.2
+ - uvicorn==0.34.0
+ - vhacdx==0.0.8.post2
+ - virtualenv==20.31.2
+ - viser==0.2.7
+ - vox2seq==0.0.0
+ - vtk==9.3.1
+ - wadler-lindig==0.1.6
+ - wandb==0.20.1
+ - warp-lang==1.7.0
+ - wcwidth==0.2.13
+ - webcolors==24.11.1
+ - webencodings==0.5.1
+ - websocket-client==1.8.0
+ - websockets==11.0.3
+ - werkzeug==3.0.6
+ - widgetsnbextension==4.0.13
+ - wn==0.12.0
+ - wrapt==1.17.2
+ - wsproto==1.2.0
+ - wurlitzer==3.1.1
+ - xatlas==0.0.10
+ - xformers==0.0.27.post2+cu118
+ - xxhash==3.5.0
+ - xyzservices==2025.4.0
+ - yacs==0.1.8
+ - yarl==1.20.0
+ - yourdfpy==0.0.58
+ - zipp==3.21.0
+ - zstandard==0.23.0
+prefix: /home/rdagli/code/miniconda3/envs/vomp
diff --git a/deps/vomp/extensions/vox2seq/benchmark.py b/deps/vomp/extensions/vox2seq/benchmark.py
new file mode 100755
index 0000000000000000000000000000000000000000..2ecb958976cf3e6e910a9924018dc69b0aac554e
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/benchmark.py
@@ -0,0 +1,68 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+import torch
+import vox2seq
+
+if __name__ == "__main__":
+ stats = {
+ "z_order_cuda": [],
+ "z_order_pytorch": [],
+ "hilbert_cuda": [],
+ "hilbert_pytorch": [],
+ }
+ RES = [16, 32, 64, 128, 256]
+ for res in RES:
+ coords = torch.meshgrid(torch.arange(res), torch.arange(res), torch.arange(res))
+ coords = torch.stack(coords, dim=-1).reshape(-1, 3).int().cuda()
+
+ start = time.time()
+ for _ in range(100):
+ code_z_cuda = vox2seq.encode(coords, mode="z_order").cuda()
+ torch.cuda.synchronize()
+ stats["z_order_cuda"].append((time.time() - start) / 100)
+
+ start = time.time()
+ for _ in range(100):
+ code_z_pytorch = vox2seq.pytorch.encode(coords, mode="z_order").cuda()
+ torch.cuda.synchronize()
+ stats["z_order_pytorch"].append((time.time() - start) / 100)
+
+ start = time.time()
+ for _ in range(100):
+ code_h_cuda = vox2seq.encode(coords, mode="hilbert").cuda()
+ torch.cuda.synchronize()
+ stats["hilbert_cuda"].append((time.time() - start) / 100)
+
+ start = time.time()
+ for _ in range(100):
+ code_h_pytorch = vox2seq.pytorch.encode(coords, mode="hilbert").cuda()
+ torch.cuda.synchronize()
+ stats["hilbert_pytorch"].append((time.time() - start) / 100)
+
+ print(
+ f"{'Resolution':<12}{'Z-Order (CUDA)':<24}{'Z-Order (PyTorch)':<24}{'Hilbert (CUDA)':<24}{'Hilbert (PyTorch)':<24}"
+ )
+ for res, z_order_cuda, z_order_pytorch, hilbert_cuda, hilbert_pytorch in zip(
+ RES,
+ stats["z_order_cuda"],
+ stats["z_order_pytorch"],
+ stats["hilbert_cuda"],
+ stats["hilbert_pytorch"],
+ ):
+ print(
+ f"{res:<12}{z_order_cuda:<24.6f}{z_order_pytorch:<24.6f}{hilbert_cuda:<24.6f}{hilbert_pytorch:<24.6f}"
+ )
diff --git a/deps/vomp/extensions/vox2seq/setup.py b/deps/vomp/extensions/vox2seq/setup.py
new file mode 100755
index 0000000000000000000000000000000000000000..b534c67435b79e5dcdc4422aad82ad6767eb8f58
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/setup.py
@@ -0,0 +1,48 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Copyright (C) 2023, Inria
+# GRAPHDECO research group, https://team.inria.fr/graphdeco
+# All rights reserved.
+#
+# This software is free for non-commercial, research and evaluation use
+# under the terms of the LICENSE.md file.
+#
+# For inquiries contact george.drettakis@inria.fr
+#
+
+from setuptools import setup
+from torch.utils.cpp_extension import CUDAExtension, BuildExtension
+import os
+
+os.path.dirname(os.path.abspath(__file__))
+
+setup(
+ name="vox2seq",
+ packages=["vox2seq", "vox2seq.pytorch"],
+ ext_modules=[
+ CUDAExtension(
+ name="vox2seq._C",
+ sources=[
+ "src/api.cu",
+ "src/z_order.cu",
+ "src/hilbert.cu",
+ "src/ext.cpp",
+ ],
+ )
+ ],
+ cmdclass={"build_ext": BuildExtension},
+)
diff --git a/deps/vomp/extensions/vox2seq/src/api.cu b/deps/vomp/extensions/vox2seq/src/api.cu
new file mode 100755
index 0000000000000000000000000000000000000000..072e930f90278f2f407b45750220d7d98c37b91e
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/api.cu
@@ -0,0 +1,92 @@
+#include
+#include "api.h"
+#include "z_order.h"
+#include "hilbert.h"
+
+
+torch::Tensor
+z_order_encode(
+ const torch::Tensor& x,
+ const torch::Tensor& y,
+ const torch::Tensor& z
+) {
+ // Allocate output tensor
+ torch::Tensor codes = torch::empty_like(x);
+
+ // Call CUDA kernel
+ z_order_encode_cuda<<<(x.size(0) + BLOCK_SIZE - 1) / BLOCK_SIZE, BLOCK_SIZE>>>(
+ x.size(0),
+ reinterpret_cast(x.contiguous().data_ptr()),
+ reinterpret_cast(y.contiguous().data_ptr()),
+ reinterpret_cast(z.contiguous().data_ptr()),
+ reinterpret_cast(codes.data_ptr())
+ );
+
+ return codes;
+}
+
+
+std::tuple
+z_order_decode(
+ const torch::Tensor& codes
+) {
+ // Allocate output tensors
+ torch::Tensor x = torch::empty_like(codes);
+ torch::Tensor y = torch::empty_like(codes);
+ torch::Tensor z = torch::empty_like(codes);
+
+ // Call CUDA kernel
+ z_order_decode_cuda<<<(codes.size(0) + BLOCK_SIZE - 1) / BLOCK_SIZE, BLOCK_SIZE>>>(
+ codes.size(0),
+ reinterpret_cast(codes.contiguous().data_ptr()),
+ reinterpret_cast(x.data_ptr()),
+ reinterpret_cast(y.data_ptr()),
+ reinterpret_cast(z.data_ptr())
+ );
+
+ return std::make_tuple(x, y, z);
+}
+
+
+torch::Tensor
+hilbert_encode(
+ const torch::Tensor& x,
+ const torch::Tensor& y,
+ const torch::Tensor& z
+) {
+ // Allocate output tensor
+ torch::Tensor codes = torch::empty_like(x);
+
+ // Call CUDA kernel
+ hilbert_encode_cuda<<<(x.size(0) + BLOCK_SIZE - 1) / BLOCK_SIZE, BLOCK_SIZE>>>(
+ x.size(0),
+ reinterpret_cast(x.contiguous().data_ptr()),
+ reinterpret_cast(y.contiguous().data_ptr()),
+ reinterpret_cast(z.contiguous().data_ptr()),
+ reinterpret_cast(codes.data_ptr())
+ );
+
+ return codes;
+}
+
+
+std::tuple
+hilbert_decode(
+ const torch::Tensor& codes
+) {
+ // Allocate output tensors
+ torch::Tensor x = torch::empty_like(codes);
+ torch::Tensor y = torch::empty_like(codes);
+ torch::Tensor z = torch::empty_like(codes);
+
+ // Call CUDA kernel
+ hilbert_decode_cuda<<<(codes.size(0) + BLOCK_SIZE - 1) / BLOCK_SIZE, BLOCK_SIZE>>>(
+ codes.size(0),
+ reinterpret_cast(codes.contiguous().data_ptr()),
+ reinterpret_cast(x.data_ptr()),
+ reinterpret_cast(y.data_ptr()),
+ reinterpret_cast(z.data_ptr())
+ );
+
+ return std::make_tuple(x, y, z);
+}
diff --git a/deps/vomp/extensions/vox2seq/src/api.h b/deps/vomp/extensions/vox2seq/src/api.h
new file mode 100755
index 0000000000000000000000000000000000000000..26a68348d56585d0e9e1dfb4900a0d23587df9a6
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/api.h
@@ -0,0 +1,76 @@
+/*
+ * Serialize a voxel grid
+ *
+ * Copyright (C) 2024, Jianfeng XIANG
+ * All rights reserved.
+ *
+ * Licensed under The MIT License [see LICENSE for details]
+ *
+ * Written by Jianfeng XIANG
+ */
+
+#pragma once
+#include
+
+
+#define BLOCK_SIZE 256
+
+
+/**
+ * Z-order encode 3D points
+ *
+ * @param x [N] tensor containing the x coordinates
+ * @param y [N] tensor containing the y coordinates
+ * @param z [N] tensor containing the z coordinates
+ *
+ * @return [N] tensor containing the z-order encoded values
+ */
+torch::Tensor
+z_order_encode(
+ const torch::Tensor& x,
+ const torch::Tensor& y,
+ const torch::Tensor& z
+);
+
+
+/**
+ * Z-order decode 3D points
+ *
+ * @param codes [N] tensor containing the z-order encoded values
+ *
+ * @return 3 tensors [N] containing the x, y, z coordinates
+ */
+std::tuple
+z_order_decode(
+ const torch::Tensor& codes
+);
+
+
+/**
+ * Hilbert encode 3D points
+ *
+ * @param x [N] tensor containing the x coordinates
+ * @param y [N] tensor containing the y coordinates
+ * @param z [N] tensor containing the z coordinates
+ *
+ * @return [N] tensor containing the Hilbert encoded values
+ */
+torch::Tensor
+hilbert_encode(
+ const torch::Tensor& x,
+ const torch::Tensor& y,
+ const torch::Tensor& z
+);
+
+
+/**
+ * Hilbert decode 3D points
+ *
+ * @param codes [N] tensor containing the Hilbert encoded values
+ *
+ * @return 3 tensors [N] containing the x, y, z coordinates
+ */
+std::tuple
+hilbert_decode(
+ const torch::Tensor& codes
+);
diff --git a/deps/vomp/extensions/vox2seq/src/ext.cpp b/deps/vomp/extensions/vox2seq/src/ext.cpp
new file mode 100755
index 0000000000000000000000000000000000000000..72e76d3b361eb8f355760f067f71005d4e37902c
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/ext.cpp
@@ -0,0 +1,10 @@
+#include
+#include "api.h"
+
+
+PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
+ m.def("z_order_encode", &z_order_encode);
+ m.def("z_order_decode", &z_order_decode);
+ m.def("hilbert_encode", &hilbert_encode);
+ m.def("hilbert_decode", &hilbert_decode);
+}
\ No newline at end of file
diff --git a/deps/vomp/extensions/vox2seq/src/hilbert.cu b/deps/vomp/extensions/vox2seq/src/hilbert.cu
new file mode 100755
index 0000000000000000000000000000000000000000..b3c5bb19474a528cf6f3102e728fa7550588ca61
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/hilbert.cu
@@ -0,0 +1,133 @@
+#include
+#include
+#include
+
+#include
+#include
+namespace cg = cooperative_groups;
+
+#include "hilbert.h"
+
+
+// Expands a 10-bit integer into 30 bits by inserting 2 zeros after each bit.
+static __device__ uint32_t expandBits(uint32_t v)
+{
+ v = (v * 0x00010001u) & 0xFF0000FFu;
+ v = (v * 0x00000101u) & 0x0F00F00Fu;
+ v = (v * 0x00000011u) & 0xC30C30C3u;
+ v = (v * 0x00000005u) & 0x49249249u;
+ return v;
+}
+
+
+// Removes 2 zeros after each bit in a 30-bit integer.
+static __device__ uint32_t extractBits(uint32_t v)
+{
+ v = v & 0x49249249;
+ v = (v ^ (v >> 2)) & 0x030C30C3u;
+ v = (v ^ (v >> 4)) & 0x0300F00Fu;
+ v = (v ^ (v >> 8)) & 0x030000FFu;
+ v = (v ^ (v >> 16)) & 0x000003FFu;
+ return v;
+}
+
+
+__global__ void hilbert_encode_cuda(
+ size_t N,
+ const uint32_t* x,
+ const uint32_t* y,
+ const uint32_t* z,
+ uint32_t* codes
+) {
+ size_t thread_id = cg::this_grid().thread_rank();
+ if (thread_id >= N) return;
+
+ uint32_t point[3] = {x[thread_id], y[thread_id], z[thread_id]};
+
+ uint32_t m = 1 << 9, q, p, t;
+
+ // Inverse undo excess work
+ q = m;
+ while (q > 1) {
+ p = q - 1;
+ for (int i = 0; i < 3; i++) {
+ if (point[i] & q) {
+ point[0] ^= p; // invert
+ } else {
+ t = (point[0] ^ point[i]) & p;
+ point[0] ^= t;
+ point[i] ^= t;
+ }
+ }
+ q >>= 1;
+ }
+
+ // Gray encode
+ for (int i = 1; i < 3; i++) {
+ point[i] ^= point[i - 1];
+ }
+ t = 0;
+ q = m;
+ while (q > 1) {
+ if (point[2] & q) {
+ t ^= q - 1;
+ }
+ q >>= 1;
+ }
+ for (int i = 0; i < 3; i++) {
+ point[i] ^= t;
+ }
+
+ // Convert to 3D Hilbert code
+ uint32_t xx = expandBits(point[0]);
+ uint32_t yy = expandBits(point[1]);
+ uint32_t zz = expandBits(point[2]);
+
+ codes[thread_id] = xx * 4 + yy * 2 + zz;
+}
+
+
+__global__ void hilbert_decode_cuda(
+ size_t N,
+ const uint32_t* codes,
+ uint32_t* x,
+ uint32_t* y,
+ uint32_t* z
+) {
+ size_t thread_id = cg::this_grid().thread_rank();
+ if (thread_id >= N) return;
+
+ uint32_t point[3];
+ point[0] = extractBits(codes[thread_id] >> 2);
+ point[1] = extractBits(codes[thread_id] >> 1);
+ point[2] = extractBits(codes[thread_id]);
+
+ uint32_t m = 2 << 9, q, p, t;
+
+ // Gray decode by H ^ (H/2)
+ t = point[2] >> 1;
+ for (int i = 2; i > 0; i--) {
+ point[i] ^= point[i - 1];
+ }
+ point[0] ^= t;
+
+ // Undo excess work
+ q = 2;
+ while (q != m) {
+ p = q - 1;
+ for (int i = 2; i >= 0; i--) {
+ if (point[i] & q) {
+ point[0] ^= p;
+ } else {
+ t = (point[0] ^ point[i]) & p;
+ point[0] ^= t;
+ point[i] ^= t;
+ }
+ }
+ q <<= 1;
+ }
+
+ x[thread_id] = point[0];
+ y[thread_id] = point[1];
+ z[thread_id] = point[2];
+}
diff --git a/deps/vomp/extensions/vox2seq/src/hilbert.h b/deps/vomp/extensions/vox2seq/src/hilbert.h
new file mode 100755
index 0000000000000000000000000000000000000000..4896bf6006f43e5e527d8bde691ce7a54b38c4d7
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/hilbert.h
@@ -0,0 +1,35 @@
+#pragma once
+
+/**
+ * Hilbert encode 3D points
+ *
+ * @param x [N] tensor containing the x coordinates
+ * @param y [N] tensor containing the y coordinates
+ * @param z [N] tensor containing the z coordinates
+ *
+ * @return [N] tensor containing the z-order encoded values
+ */
+__global__ void hilbert_encode_cuda(
+ size_t N,
+ const uint32_t* x,
+ const uint32_t* y,
+ const uint32_t* z,
+ uint32_t* codes
+);
+
+
+/**
+ * Hilbert decode 3D points
+ *
+ * @param codes [N] tensor containing the z-order encoded values
+ * @param x [N] tensor containing the x coordinates
+ * @param y [N] tensor containing the y coordinates
+ * @param z [N] tensor containing the z coordinates
+ */
+__global__ void hilbert_decode_cuda(
+ size_t N,
+ const uint32_t* codes,
+ uint32_t* x,
+ uint32_t* y,
+ uint32_t* z
+);
diff --git a/deps/vomp/extensions/vox2seq/src/z_order.cu b/deps/vomp/extensions/vox2seq/src/z_order.cu
new file mode 100755
index 0000000000000000000000000000000000000000..ba6f5a91e55588d1ca4bea7cb45e6936330a694b
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/z_order.cu
@@ -0,0 +1,66 @@
+#include
+#include
+#include
+
+#include
+#include
+namespace cg = cooperative_groups;
+
+#include "z_order.h"
+
+
+// Expands a 10-bit integer into 30 bits by inserting 2 zeros after each bit.
+static __device__ uint32_t expandBits(uint32_t v)
+{
+ v = (v * 0x00010001u) & 0xFF0000FFu;
+ v = (v * 0x00000101u) & 0x0F00F00Fu;
+ v = (v * 0x00000011u) & 0xC30C30C3u;
+ v = (v * 0x00000005u) & 0x49249249u;
+ return v;
+}
+
+
+// Removes 2 zeros after each bit in a 30-bit integer.
+static __device__ uint32_t extractBits(uint32_t v)
+{
+ v = v & 0x49249249;
+ v = (v ^ (v >> 2)) & 0x030C30C3u;
+ v = (v ^ (v >> 4)) & 0x0300F00Fu;
+ v = (v ^ (v >> 8)) & 0x030000FFu;
+ v = (v ^ (v >> 16)) & 0x000003FFu;
+ return v;
+}
+
+
+__global__ void z_order_encode_cuda(
+ size_t N,
+ const uint32_t* x,
+ const uint32_t* y,
+ const uint32_t* z,
+ uint32_t* codes
+) {
+ size_t thread_id = cg::this_grid().thread_rank();
+ if (thread_id >= N) return;
+
+ uint32_t xx = expandBits(x[thread_id]);
+ uint32_t yy = expandBits(y[thread_id]);
+ uint32_t zz = expandBits(z[thread_id]);
+
+ codes[thread_id] = xx * 4 + yy * 2 + zz;
+}
+
+
+__global__ void z_order_decode_cuda(
+ size_t N,
+ const uint32_t* codes,
+ uint32_t* x,
+ uint32_t* y,
+ uint32_t* z
+) {
+ size_t thread_id = cg::this_grid().thread_rank();
+ if (thread_id >= N) return;
+
+ x[thread_id] = extractBits(codes[thread_id] >> 2);
+ y[thread_id] = extractBits(codes[thread_id] >> 1);
+ z[thread_id] = extractBits(codes[thread_id]);
+}
diff --git a/deps/vomp/extensions/vox2seq/src/z_order.h b/deps/vomp/extensions/vox2seq/src/z_order.h
new file mode 100755
index 0000000000000000000000000000000000000000..a4aa857d064e375c8f2eb023abd9ac4af5a2d8f5
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/src/z_order.h
@@ -0,0 +1,35 @@
+#pragma once
+
+/**
+ * Z-order encode 3D points
+ *
+ * @param x [N] tensor containing the x coordinates
+ * @param y [N] tensor containing the y coordinates
+ * @param z [N] tensor containing the z coordinates
+ *
+ * @return [N] tensor containing the z-order encoded values
+ */
+__global__ void z_order_encode_cuda(
+ size_t N,
+ const uint32_t* x,
+ const uint32_t* y,
+ const uint32_t* z,
+ uint32_t* codes
+);
+
+
+/**
+ * Z-order decode 3D points
+ *
+ * @param codes [N] tensor containing the z-order encoded values
+ * @param x [N] tensor containing the x coordinates
+ * @param y [N] tensor containing the y coordinates
+ * @param z [N] tensor containing the z coordinates
+ */
+__global__ void z_order_decode_cuda(
+ size_t N,
+ const uint32_t* codes,
+ uint32_t* x,
+ uint32_t* y,
+ uint32_t* z
+);
diff --git a/deps/vomp/extensions/vox2seq/test.py b/deps/vomp/extensions/vox2seq/test.py
new file mode 100755
index 0000000000000000000000000000000000000000..1ba682cfdcda0e5298526d8e6c5f35e7eefa5f45
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/test.py
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import vox2seq
+
+if __name__ == "__main__":
+ RES = 256
+ coords = torch.meshgrid(torch.arange(RES), torch.arange(RES), torch.arange(RES))
+ coords = torch.stack(coords, dim=-1).reshape(-1, 3).int().cuda()
+ code_z_cuda = vox2seq.encode(coords, mode="z_order")
+ code_z_pytorch = vox2seq.pytorch.encode(coords, mode="z_order")
+ code_h_cuda = vox2seq.encode(coords, mode="hilbert")
+ code_h_pytorch = vox2seq.pytorch.encode(coords, mode="hilbert")
+ assert torch.equal(code_z_cuda, code_z_pytorch)
+ assert torch.equal(code_h_cuda, code_h_pytorch)
+
+ code = torch.arange(RES**3).int().cuda()
+ coords_z_cuda = vox2seq.decode(code, mode="z_order")
+ coords_z_pytorch = vox2seq.pytorch.decode(code, mode="z_order")
+ coords_h_cuda = vox2seq.decode(code, mode="hilbert")
+ coords_h_pytorch = vox2seq.pytorch.decode(code, mode="hilbert")
+ assert torch.equal(coords_z_cuda, coords_z_pytorch)
+ assert torch.equal(coords_h_cuda, coords_h_pytorch)
+
+ print("All tests passed.")
diff --git a/deps/vomp/extensions/vox2seq/vox2seq/__init__.py b/deps/vomp/extensions/vox2seq/vox2seq/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..2ba93b482d7dec92e19b0e5e540bd72a3030ddd0
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/vox2seq/__init__.py
@@ -0,0 +1,74 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+from . import _C
+from . import pytorch
+
+
+@torch.no_grad()
+def encode(
+ coords: torch.Tensor,
+ permute: List[int] = [0, 1, 2],
+ mode: Literal["z_order", "hilbert"] = "z_order",
+) -> torch.Tensor:
+ """
+ Encodes 3D coordinates into a 30-bit code.
+
+ Args:
+ coords: a tensor of shape [N, 3] containing the 3D coordinates.
+ permute: the permutation of the coordinates.
+ mode: the encoding mode to use.
+ """
+ assert (
+ coords.shape[-1] == 3 and coords.ndim == 2
+ ), "Input coordinates must be of shape [N, 3]"
+ x = coords[:, permute[0]].int()
+ y = coords[:, permute[1]].int()
+ z = coords[:, permute[2]].int()
+ if mode == "z_order":
+ return _C.z_order_encode(x, y, z)
+ elif mode == "hilbert":
+ return _C.hilbert_encode(x, y, z)
+ else:
+ raise ValueError(f"Unknown encoding mode: {mode}")
+
+
+@torch.no_grad()
+def decode(
+ code: torch.Tensor,
+ permute: List[int] = [0, 1, 2],
+ mode: Literal["z_order", "hilbert"] = "z_order",
+) -> torch.Tensor:
+ """
+ Decodes a 30-bit code into 3D coordinates.
+
+ Args:
+ code: a tensor of shape [N] containing the 30-bit code.
+ permute: the permutation of the coordinates.
+ mode: the decoding mode to use.
+ """
+ assert code.ndim == 1, "Input code must be of shape [N]"
+ if mode == "z_order":
+ coords = _C.z_order_decode(code)
+ elif mode == "hilbert":
+ coords = _C.hilbert_decode(code)
+ else:
+ raise ValueError(f"Unknown decoding mode: {mode}")
+ x = coords[permute.index(0)]
+ y = coords[permute.index(1)]
+ z = coords[permute.index(2)]
+ return torch.stack([x, y, z], dim=-1)
diff --git a/deps/vomp/extensions/vox2seq/vox2seq/pytorch/__init__.py b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..f30f85cd92ce6bc3b31c1cdade345b1762da4bce
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/__init__.py
@@ -0,0 +1,70 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+from typing import *
+
+from .default import (
+ encode,
+ decode,
+ z_order_encode,
+ z_order_decode,
+ hilbert_encode,
+ hilbert_decode,
+)
+
+
+@torch.no_grad()
+def encode(
+ coords: torch.Tensor,
+ permute: List[int] = [0, 1, 2],
+ mode: Literal["z_order", "hilbert"] = "z_order",
+) -> torch.Tensor:
+ """
+ Encodes 3D coordinates into a 30-bit code.
+
+ Args:
+ coords: a tensor of shape [N, 3] containing the 3D coordinates.
+ permute: the permutation of the coordinates.
+ mode: the encoding mode to use.
+ """
+ if mode == "z_order":
+ return z_order_encode(coords[:, permute], depth=10).int()
+ elif mode == "hilbert":
+ return hilbert_encode(coords[:, permute], depth=10).int()
+ else:
+ raise ValueError(f"Unknown encoding mode: {mode}")
+
+
+@torch.no_grad()
+def decode(
+ code: torch.Tensor,
+ permute: List[int] = [0, 1, 2],
+ mode: Literal["z_order", "hilbert"] = "z_order",
+) -> torch.Tensor:
+ """
+ Decodes a 30-bit code into 3D coordinates.
+
+ Args:
+ code: a tensor of shape [N] containing the 30-bit code.
+ permute: the permutation of the coordinates.
+ mode: the decoding mode to use.
+ """
+ if mode == "z_order":
+ return z_order_decode(code, depth=10)[:, permute].float()
+ elif mode == "hilbert":
+ return hilbert_decode(code, depth=10)[:, permute].float()
+ else:
+ raise ValueError(f"Unknown decoding mode: {mode}")
diff --git a/deps/vomp/extensions/vox2seq/vox2seq/pytorch/default.py b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/default.py
new file mode 100755
index 0000000000000000000000000000000000000000..fbc7a306036b1fc2ab212cfd565cb4f1845caa20
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/default.py
@@ -0,0 +1,74 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+from .z_order import xyz2key as z_order_encode_
+from .z_order import key2xyz as z_order_decode_
+from .hilbert import encode as hilbert_encode_
+from .hilbert import decode as hilbert_decode_
+
+
+@torch.inference_mode()
+def encode(grid_coord, batch=None, depth=16, order="z"):
+ assert order in {"z", "z-trans", "hilbert", "hilbert-trans"}
+ if order == "z":
+ code = z_order_encode(grid_coord, depth=depth)
+ elif order == "z-trans":
+ code = z_order_encode(grid_coord[:, [1, 0, 2]], depth=depth)
+ elif order == "hilbert":
+ code = hilbert_encode(grid_coord, depth=depth)
+ elif order == "hilbert-trans":
+ code = hilbert_encode(grid_coord[:, [1, 0, 2]], depth=depth)
+ else:
+ raise NotImplementedError
+ if batch is not None:
+ batch = batch.long()
+ code = batch << depth * 3 | code
+ return code
+
+
+@torch.inference_mode()
+def decode(code, depth=16, order="z"):
+ assert order in {"z", "hilbert"}
+ batch = code >> depth * 3
+ code = code & ((1 << depth * 3) - 1)
+ if order == "z":
+ grid_coord = z_order_decode(code, depth=depth)
+ elif order == "hilbert":
+ grid_coord = hilbert_decode(code, depth=depth)
+ else:
+ raise NotImplementedError
+ return grid_coord, batch
+
+
+def z_order_encode(grid_coord: torch.Tensor, depth: int = 16):
+ x, y, z = grid_coord[:, 0].long(), grid_coord[:, 1].long(), grid_coord[:, 2].long()
+ # we block the support to batch, maintain batched code in Point class
+ code = z_order_encode_(x, y, z, b=None, depth=depth)
+ return code
+
+
+def z_order_decode(code: torch.Tensor, depth):
+ x, y, z, _ = z_order_decode_(code, depth=depth)
+ grid_coord = torch.stack([x, y, z], dim=-1) # (N, 3)
+ return grid_coord
+
+
+def hilbert_encode(grid_coord: torch.Tensor, depth: int = 16):
+ return hilbert_encode_(grid_coord, num_dims=3, num_bits=depth)
+
+
+def hilbert_decode(code: torch.Tensor, depth: int = 16):
+ return hilbert_decode_(code, num_dims=3, num_bits=depth)
diff --git a/deps/vomp/extensions/vox2seq/vox2seq/pytorch/hilbert.py b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/hilbert.py
new file mode 100755
index 0000000000000000000000000000000000000000..13354202ece6b9481b93083488d8f08ee9d4ff69
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/hilbert.py
@@ -0,0 +1,318 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Hilbert Order
+Modified from https://github.com/PrincetonLIPS/numpy-hilbert-curve
+
+Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Kaixin Xu
+Please cite our work if the code is helpful to you.
+"""
+
+import torch
+
+
+def right_shift(binary, k=1, axis=-1):
+ """Right shift an array of binary values.
+
+ Parameters:
+ -----------
+ binary: An ndarray of binary values.
+
+ k: The number of bits to shift. Default 1.
+
+ axis: The axis along which to shift. Default -1.
+
+ Returns:
+ --------
+ Returns an ndarray with zero prepended and the ends truncated, along
+ whatever axis was specified."""
+
+ # If we're shifting the whole thing, just return zeros.
+ if binary.shape[axis] <= k:
+ return torch.zeros_like(binary)
+
+ # Determine the padding pattern.
+ # padding = [(0,0)] * len(binary.shape)
+ # padding[axis] = (k,0)
+
+ # Determine the slicing pattern to eliminate just the last one.
+ slicing = [slice(None)] * len(binary.shape)
+ slicing[axis] = slice(None, -k)
+ shifted = torch.nn.functional.pad(
+ binary[tuple(slicing)], (k, 0), mode="constant", value=0
+ )
+
+ return shifted
+
+
+def binary2gray(binary, axis=-1):
+ """Convert an array of binary values into Gray codes.
+
+ This uses the classic X ^ (X >> 1) trick to compute the Gray code.
+
+ Parameters:
+ -----------
+ binary: An ndarray of binary values.
+
+ axis: The axis along which to compute the gray code. Default=-1.
+
+ Returns:
+ --------
+ Returns an ndarray of Gray codes.
+ """
+ shifted = right_shift(binary, axis=axis)
+
+ # Do the X ^ (X >> 1) trick.
+ gray = torch.logical_xor(binary, shifted)
+
+ return gray
+
+
+def gray2binary(gray, axis=-1):
+ """Convert an array of Gray codes back into binary values.
+
+ Parameters:
+ -----------
+ gray: An ndarray of gray codes.
+
+ axis: The axis along which to perform Gray decoding. Default=-1.
+
+ Returns:
+ --------
+ Returns an ndarray of binary values.
+ """
+
+ # Loop the log2(bits) number of times necessary, with shift and xor.
+ shift = 2 ** (torch.Tensor([gray.shape[axis]]).log2().ceil().int() - 1)
+ while shift > 0:
+ gray = torch.logical_xor(gray, right_shift(gray, shift))
+ shift = torch.div(shift, 2, rounding_mode="floor")
+ return gray
+
+
+def encode(locs, num_dims, num_bits):
+ """Decode an array of locations in a hypercube into a Hilbert integer.
+
+ This is a vectorized-ish version of the Hilbert curve implementation by John
+ Skilling as described in:
+
+ Skilling, J. (2004, April). Programming the Hilbert curve. In AIP Conference
+ Proceedings (Vol. 707, No. 1, pp. 381-387). American Institute of Physics.
+
+ Params:
+ -------
+ locs - An ndarray of locations in a hypercube of num_dims dimensions, in
+ which each dimension runs from 0 to 2**num_bits-1. The shape can
+ be arbitrary, as long as the last dimension of the same has size
+ num_dims.
+
+ num_dims - The dimensionality of the hypercube. Integer.
+
+ num_bits - The number of bits for each dimension. Integer.
+
+ Returns:
+ --------
+ The output is an ndarray of uint64 integers with the same shape as the
+ input, excluding the last dimension, which needs to be num_dims.
+ """
+
+ # Keep around the original shape for later.
+ orig_shape = locs.shape
+ bitpack_mask = 1 << torch.arange(0, 8).to(locs.device)
+ bitpack_mask_rev = bitpack_mask.flip(-1)
+
+ if orig_shape[-1] != num_dims:
+ raise ValueError(
+ """
+ The shape of locs was surprising in that the last dimension was of size
+ %d, but num_dims=%d. These need to be equal.
+ """
+ % (orig_shape[-1], num_dims)
+ )
+
+ if num_dims * num_bits > 63:
+ raise ValueError(
+ """
+ num_dims=%d and num_bits=%d for %d bits total, which can't be encoded
+ into a int64. Are you sure you need that many points on your Hilbert
+ curve?
+ """
+ % (num_dims, num_bits, num_dims * num_bits)
+ )
+
+ # Treat the location integers as 64-bit unsigned and then split them up into
+ # a sequence of uint8s. Preserve the association by dimension.
+ locs_uint8 = locs.long().view(torch.uint8).reshape((-1, num_dims, 8)).flip(-1)
+
+ # Now turn these into bits and truncate to num_bits.
+ gray = (
+ locs_uint8.unsqueeze(-1)
+ .bitwise_and(bitpack_mask_rev)
+ .ne(0)
+ .byte()
+ .flatten(-2, -1)[..., -num_bits:]
+ )
+
+ # Run the decoding process the other way.
+ # Iterate forwards through the bits.
+ for bit in range(0, num_bits):
+ # Iterate forwards through the dimensions.
+ for dim in range(0, num_dims):
+ # Identify which ones have this bit active.
+ mask = gray[:, dim, bit]
+
+ # Where this bit is on, invert the 0 dimension for lower bits.
+ gray[:, 0, bit + 1 :] = torch.logical_xor(
+ gray[:, 0, bit + 1 :], mask[:, None]
+ )
+
+ # Where the bit is off, exchange the lower bits with the 0 dimension.
+ to_flip = torch.logical_and(
+ torch.logical_not(mask[:, None]).repeat(1, gray.shape[2] - bit - 1),
+ torch.logical_xor(gray[:, 0, bit + 1 :], gray[:, dim, bit + 1 :]),
+ )
+ gray[:, dim, bit + 1 :] = torch.logical_xor(
+ gray[:, dim, bit + 1 :], to_flip
+ )
+ gray[:, 0, bit + 1 :] = torch.logical_xor(gray[:, 0, bit + 1 :], to_flip)
+
+ # Now flatten out.
+ gray = gray.swapaxes(1, 2).reshape((-1, num_bits * num_dims))
+
+ # Convert Gray back to binary.
+ hh_bin = gray2binary(gray)
+
+ # Pad back out to 64 bits.
+ extra_dims = 64 - num_bits * num_dims
+ padded = torch.nn.functional.pad(hh_bin, (extra_dims, 0), "constant", 0)
+
+ # Convert binary values into uint8s.
+ hh_uint8 = (
+ (padded.flip(-1).reshape((-1, 8, 8)) * bitpack_mask)
+ .sum(2)
+ .squeeze()
+ .type(torch.uint8)
+ )
+
+ # Convert uint8s into uint64s.
+ hh_uint64 = hh_uint8.view(torch.int64).squeeze()
+
+ return hh_uint64
+
+
+def decode(hilberts, num_dims, num_bits):
+ """Decode an array of Hilbert integers into locations in a hypercube.
+
+ This is a vectorized-ish version of the Hilbert curve implementation by John
+ Skilling as described in:
+
+ Skilling, J. (2004, April). Programming the Hilbert curve. In AIP Conference
+ Proceedings (Vol. 707, No. 1, pp. 381-387). American Institute of Physics.
+
+ Params:
+ -------
+ hilberts - An ndarray of Hilbert integers. Must be an integer dtype and
+ cannot have fewer bits than num_dims * num_bits.
+
+ num_dims - The dimensionality of the hypercube. Integer.
+
+ num_bits - The number of bits for each dimension. Integer.
+
+ Returns:
+ --------
+ The output is an ndarray of unsigned integers with the same shape as hilberts
+ but with an additional dimension of size num_dims.
+ """
+
+ if num_dims * num_bits > 64:
+ raise ValueError(
+ """
+ num_dims=%d and num_bits=%d for %d bits total, which can't be encoded
+ into a uint64. Are you sure you need that many points on your Hilbert
+ curve?
+ """
+ % (num_dims, num_bits)
+ )
+
+ # Handle the case where we got handed a naked integer.
+ hilberts = torch.atleast_1d(hilberts)
+
+ # Keep around the shape for later.
+ orig_shape = hilberts.shape
+ bitpack_mask = 2 ** torch.arange(0, 8).to(hilberts.device)
+ bitpack_mask_rev = bitpack_mask.flip(-1)
+
+ # Treat each of the hilberts as a s equence of eight uint8.
+ # This treats all of the inputs as uint64 and makes things uniform.
+ hh_uint8 = (
+ hilberts.ravel().type(torch.int64).view(torch.uint8).reshape((-1, 8)).flip(-1)
+ )
+
+ # Turn these lists of uints into lists of bits and then truncate to the size
+ # we actually need for using Skilling's procedure.
+ hh_bits = (
+ hh_uint8.unsqueeze(-1)
+ .bitwise_and(bitpack_mask_rev)
+ .ne(0)
+ .byte()
+ .flatten(-2, -1)[:, -num_dims * num_bits :]
+ )
+
+ # Take the sequence of bits and Gray-code it.
+ gray = binary2gray(hh_bits)
+
+ # There has got to be a better way to do this.
+ # I could index them differently, but the eventual packbits likes it this way.
+ gray = gray.reshape((-1, num_bits, num_dims)).swapaxes(1, 2)
+
+ # Iterate backwards through the bits.
+ for bit in range(num_bits - 1, -1, -1):
+ # Iterate backwards through the dimensions.
+ for dim in range(num_dims - 1, -1, -1):
+ # Identify which ones have this bit active.
+ mask = gray[:, dim, bit]
+
+ # Where this bit is on, invert the 0 dimension for lower bits.
+ gray[:, 0, bit + 1 :] = torch.logical_xor(
+ gray[:, 0, bit + 1 :], mask[:, None]
+ )
+
+ # Where the bit is off, exchange the lower bits with the 0 dimension.
+ to_flip = torch.logical_and(
+ torch.logical_not(mask[:, None]),
+ torch.logical_xor(gray[:, 0, bit + 1 :], gray[:, dim, bit + 1 :]),
+ )
+ gray[:, dim, bit + 1 :] = torch.logical_xor(
+ gray[:, dim, bit + 1 :], to_flip
+ )
+ gray[:, 0, bit + 1 :] = torch.logical_xor(gray[:, 0, bit + 1 :], to_flip)
+
+ # Pad back out to 64 bits.
+ extra_dims = 64 - num_bits
+ padded = torch.nn.functional.pad(gray, (extra_dims, 0), "constant", 0)
+
+ # Now chop these up into blocks of 8.
+ locs_chopped = padded.flip(-1).reshape((-1, num_dims, 8, 8))
+
+ # Take those blocks and turn them unto uint8s.
+ # from IPython import embed; embed()
+ locs_uint8 = (locs_chopped * bitpack_mask).sum(3).squeeze().type(torch.uint8)
+
+ # Finally, treat these as uint64s.
+ flat_locs = locs_uint8.view(torch.int64)
+
+ # Return them in the expected shape.
+ return flat_locs.reshape((*orig_shape, num_dims))
diff --git a/deps/vomp/extensions/vox2seq/vox2seq/pytorch/z_order.py b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/z_order.py
new file mode 100755
index 0000000000000000000000000000000000000000..83194e1048cd58c7783465431687b04216f69c0b
--- /dev/null
+++ b/deps/vomp/extensions/vox2seq/vox2seq/pytorch/z_order.py
@@ -0,0 +1,141 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# --------------------------------------------------------
+# Octree-based Sparse Convolutional Neural Networks
+# Copyright (c) 2022 Peng-Shuai Wang
+# Licensed under The MIT License [see LICENSE for details]
+# Written by Peng-Shuai Wang
+# --------------------------------------------------------
+
+import torch
+from typing import Optional, Union
+
+
+class KeyLUT:
+ def __init__(self):
+ r256 = torch.arange(256, dtype=torch.int64)
+ r512 = torch.arange(512, dtype=torch.int64)
+ zero = torch.zeros(256, dtype=torch.int64)
+ device = torch.device("cpu")
+
+ self._encode = {
+ device: (
+ self.xyz2key(r256, zero, zero, 8),
+ self.xyz2key(zero, r256, zero, 8),
+ self.xyz2key(zero, zero, r256, 8),
+ )
+ }
+ self._decode = {device: self.key2xyz(r512, 9)}
+
+ def encode_lut(self, device=torch.device("cpu")):
+ if device not in self._encode:
+ cpu = torch.device("cpu")
+ self._encode[device] = tuple(e.to(device) for e in self._encode[cpu])
+ return self._encode[device]
+
+ def decode_lut(self, device=torch.device("cpu")):
+ if device not in self._decode:
+ cpu = torch.device("cpu")
+ self._decode[device] = tuple(e.to(device) for e in self._decode[cpu])
+ return self._decode[device]
+
+ def xyz2key(self, x, y, z, depth):
+ key = torch.zeros_like(x)
+ for i in range(depth):
+ mask = 1 << i
+ key = (
+ key
+ | ((x & mask) << (2 * i + 2))
+ | ((y & mask) << (2 * i + 1))
+ | ((z & mask) << (2 * i + 0))
+ )
+ return key
+
+ def key2xyz(self, key, depth):
+ x = torch.zeros_like(key)
+ y = torch.zeros_like(key)
+ z = torch.zeros_like(key)
+ for i in range(depth):
+ x = x | ((key & (1 << (3 * i + 2))) >> (2 * i + 2))
+ y = y | ((key & (1 << (3 * i + 1))) >> (2 * i + 1))
+ z = z | ((key & (1 << (3 * i + 0))) >> (2 * i + 0))
+ return x, y, z
+
+
+_key_lut = KeyLUT()
+
+
+def xyz2key(
+ x: torch.Tensor,
+ y: torch.Tensor,
+ z: torch.Tensor,
+ b: Optional[Union[torch.Tensor, int]] = None,
+ depth: int = 16,
+):
+ r"""Encodes :attr:`x`, :attr:`y`, :attr:`z` coordinates to the shuffled keys
+ based on pre-computed look up tables. The speed of this function is much
+ faster than the method based on for-loop.
+
+ Args:
+ x (torch.Tensor): The x coordinate.
+ y (torch.Tensor): The y coordinate.
+ z (torch.Tensor): The z coordinate.
+ b (torch.Tensor or int): The batch index of the coordinates, and should be
+ smaller than 32768. If :attr:`b` is :obj:`torch.Tensor`, the size of
+ :attr:`b` must be the same as :attr:`x`, :attr:`y`, and :attr:`z`.
+ depth (int): The depth of the shuffled key, and must be smaller than 17 (< 17).
+ """
+
+ EX, EY, EZ = _key_lut.encode_lut(x.device)
+ x, y, z = x.long(), y.long(), z.long()
+
+ mask = 255 if depth > 8 else (1 << depth) - 1
+ key = EX[x & mask] | EY[y & mask] | EZ[z & mask]
+ if depth > 8:
+ mask = (1 << (depth - 8)) - 1
+ key16 = EX[(x >> 8) & mask] | EY[(y >> 8) & mask] | EZ[(z >> 8) & mask]
+ key = key16 << 24 | key
+
+ if b is not None:
+ b = b.long()
+ key = b << 48 | key
+
+ return key
+
+
+def key2xyz(key: torch.Tensor, depth: int = 16):
+ r"""Decodes the shuffled key to :attr:`x`, :attr:`y`, :attr:`z` coordinates
+ and the batch index based on pre-computed look up tables.
+
+ Args:
+ key (torch.Tensor): The shuffled key.
+ depth (int): The depth of the shuffled key, and must be smaller than 17 (< 17).
+ """
+
+ DX, DY, DZ = _key_lut.decode_lut(key.device)
+ x, y, z = torch.zeros_like(key), torch.zeros_like(key), torch.zeros_like(key)
+
+ b = key >> 48
+ key = key & ((1 << 48) - 1)
+
+ n = (depth + 2) // 3
+ for i in range(n):
+ k = key >> (i * 9) & 511
+ x = x | (DX[k] << (i * 3))
+ y = y | (DY[k] << (i * 3))
+ z = z | (DZ[k] << (i * 3))
+
+ return x, y, z, b
diff --git a/deps/vomp/gradio/README.md b/deps/vomp/gradio/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9817354f59259fa59ab1f4c04491a01001cf3b1f
--- /dev/null
+++ b/deps/vomp/gradio/README.md
@@ -0,0 +1,14 @@
+---
+title: VoMP
+emoji: ๐
+colorFrom: green
+colorTo: green
+sdk: gradio
+sdk_version: 6.2.0
+app_file: app.py
+pinned: true
+license: apache-2.0
+short_description: Generate volumetric mechanical properties from objects for interactive worlds
+suggested_hardware: a100-large
+suggested_storage: medium
+---
\ No newline at end of file
diff --git a/deps/vomp/gradio/app.py b/deps/vomp/gradio/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f3aeae9586a808b3000f79616f77df050f979d2
--- /dev/null
+++ b/deps/vomp/gradio/app.py
@@ -0,0 +1,561 @@
+import glob
+import os
+import shutil
+import tempfile
+from typing import Dict, List, Optional, Tuple
+
+import gradio as gr
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import matplotlib.colors as mcolors
+from matplotlib.colorbar import ColorbarBase
+import numpy as np
+import spaces
+import torch
+
+from vomp.inference import Vomp
+from vomp.inference.utils import save_materials
+
+NUM_VIEWS = 150
+PROPERTY_NAMES = ["youngs_modulus", "poissons_ratio", "density"]
+PROPERTY_DISPLAY_NAMES = {
+ "youngs_modulus": "Young's Modulus",
+ "poissons_ratio": "Poisson's Ratio",
+ "density": "Density",
+}
+
+BLENDER_LINK = (
+ "https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
+)
+BLENDER_INSTALLATION_PATH = "/tmp"
+BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
+
+EXAMPLES_DIR = "examples"
+
+
+def _install_blender():
+ if not os.path.exists(BLENDER_PATH):
+ print("Installing Blender...")
+ os.system("sudo apt-get update")
+ os.system(
+ "sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
+ )
+ os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
+ os.system(
+ f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
+ )
+ print("Blender installed successfully!")
+
+
+def _is_gaussian_splat(file_path: str) -> bool:
+ if not file_path.lower().endswith(".ply"):
+ return False
+
+ try:
+ with open(file_path, "rb") as f:
+ header = b""
+ while True:
+ line = f.readline()
+ header += line
+ if b"end_header" in line:
+ break
+ if len(header) > 10000:
+ break
+
+ header_str = header.decode("utf-8", errors="ignore").lower()
+ gaussian_indicators = ["f_dc", "opacity", "scale_0", "rot_0"]
+ return any(indicator in header_str for indicator in gaussian_indicators)
+ except Exception:
+ return False
+
+
+def _setup_examples():
+ """Ensure examples directory exists."""
+ os.makedirs(EXAMPLES_DIR, exist_ok=True)
+
+
+_setup_examples()
+
+
+print("Loading VoMP model...")
+model = Vomp.from_checkpoint(
+ config_path="weights/inference.json",
+ geometry_checkpoint_dir="weights/geometry_transformer.pt",
+ matvae_checkpoint_dir="weights/matvae.safetensors",
+ normalization_params_path="weights/normalization_params.json",
+)
+print("VoMP model loaded successfully!")
+
+
+def _get_render_images(output_dir: str) -> List[str]:
+ renders_dir = os.path.join(output_dir, "renders")
+ if not os.path.exists(renders_dir):
+ return []
+ image_paths = sorted(glob.glob(os.path.join(renders_dir, "*.png")))
+ return image_paths
+
+
+def _create_colorbar(
+ data: np.ndarray, property_name: str, output_path: str, colormap: str = "viridis"
+) -> str:
+ fig, ax = plt.subplots(figsize=(6, 0.8))
+ fig.subplots_adjust(bottom=0.5)
+ ax.remove()
+
+ cmap = plt.cm.get_cmap(colormap)
+ norm = mcolors.Normalize(vmin=np.min(data), vmax=np.max(data))
+
+ cbar_ax = fig.add_axes([0.1, 0.4, 0.8, 0.35])
+ cb = ColorbarBase(cbar_ax, cmap=cmap, norm=norm, orientation="horizontal")
+ cb.ax.set_xlabel(
+ f"{PROPERTY_DISPLAY_NAMES.get(property_name, property_name)}", fontsize=10
+ )
+
+ plt.savefig(
+ output_path, dpi=150, bbox_inches="tight", facecolor="white", transparent=False
+ )
+ plt.close()
+ return output_path
+
+
+def _render_point_cloud_views(
+ coords: np.ndarray,
+ values: np.ndarray,
+ output_dir: str,
+ property_name: str,
+ colormap: str = "viridis",
+) -> List[str]:
+ vmin, vmax = np.min(values), np.max(values)
+ if vmax - vmin > 1e-10:
+ normalized = (values - vmin) / (vmax - vmin)
+ else:
+ normalized = np.zeros_like(values)
+
+ cmap = plt.cm.get_cmap(colormap)
+ colors = cmap(normalized)
+
+ views = [
+ (30, 45, "view1"),
+ (30, 135, "view2"),
+ (80, 45, "view3"),
+ ]
+
+ image_paths = []
+
+ for elev, azim, view_name in views:
+ fig = plt.figure(figsize=(6, 6), facecolor="#1a1a1a")
+ ax = fig.add_subplot(111, projection="3d", facecolor="#1a1a1a")
+
+ ax.scatter(
+ coords[:, 0],
+ coords[:, 1],
+ coords[:, 2],
+ c=colors,
+ s=15,
+ alpha=0.9,
+ )
+
+ ax.view_init(elev=elev, azim=azim)
+ ax.set_xlim([-0.6, 0.6])
+ ax.set_ylim([-0.6, 0.6])
+ ax.set_zlim([-0.6, 0.6])
+ ax.set_axis_off()
+ ax.set_box_aspect([1, 1, 1])
+
+ output_path = os.path.join(output_dir, f"{property_name}_{view_name}.png")
+ plt.savefig(
+ output_path,
+ dpi=150,
+ bbox_inches="tight",
+ facecolor="#1a1a1a",
+ edgecolor="none",
+ )
+ plt.close()
+
+ image_paths.append(output_path)
+
+ return image_paths
+
+
+def _create_material_visualizations(
+ material_file: str, output_dir: str
+) -> Dict[str, Tuple[List[str], str]]:
+ result = {}
+ data = np.load(material_file, allow_pickle=True)
+
+ if "voxel_data" in data:
+ voxel_data = data["voxel_data"]
+ coords = np.column_stack([voxel_data["x"], voxel_data["y"], voxel_data["z"]])
+ properties = {
+ "youngs_modulus": voxel_data["youngs_modulus"],
+ "poissons_ratio": voxel_data["poissons_ratio"],
+ "density": voxel_data["density"],
+ }
+ else:
+ if "voxel_coords_world" in data:
+ coords = data["voxel_coords_world"]
+ elif "query_coords_world" in data:
+ coords = data["query_coords_world"]
+ elif "coords" in data:
+ coords = data["coords"]
+ else:
+ print(f"Warning: No coordinate data found in {material_file}")
+ return result
+
+ properties = {}
+ property_mapping = {
+ "youngs_modulus": ["youngs_modulus", "young_modulus"],
+ "poissons_ratio": ["poissons_ratio", "poisson_ratio"],
+ "density": ["density"],
+ }
+ for prop_name, possible_names in property_mapping.items():
+ for name in possible_names:
+ if name in data:
+ properties[prop_name] = data[name]
+ break
+
+ center = (np.min(coords, axis=0) + np.max(coords, axis=0)) / 2
+ max_range = np.max(np.max(coords, axis=0) - np.min(coords, axis=0))
+ if max_range > 1e-10:
+ coords_normalized = (coords - center) / max_range
+ else:
+ coords_normalized = coords - center
+
+ for prop_name, prop_data in properties.items():
+ if prop_data is not None:
+ view_paths = _render_point_cloud_views(
+ coords_normalized, prop_data, output_dir, prop_name
+ )
+ colorbar_path = os.path.join(output_dir, f"{prop_name}_colorbar.png")
+ _create_colorbar(prop_data, prop_name, colorbar_path)
+ result[prop_name] = (view_paths, colorbar_path)
+ print(f"Created visualization for {prop_name}: {len(view_paths)} views")
+
+ return result
+
+
+@spaces.GPU(duration=60)
+@torch.no_grad()
+def process_3d_model(input_file):
+ empty_result = (
+ None,
+ [],
+ None,
+ [],
+ None,
+ None,
+ [],
+ None,
+ None,
+ [],
+ None,
+ None,
+ )
+
+ if input_file is None:
+ return empty_result
+
+ output_dir = tempfile.mkdtemp(prefix="vomp_")
+ material_file = os.path.join(output_dir, "materials.npz")
+
+ try:
+ if _is_gaussian_splat(input_file):
+ print(f"Processing as Gaussian splat: {input_file}")
+ results = model.get_splat_materials(
+ input_file,
+ voxel_method="kaolin",
+ query_points="voxel_centers",
+ output_dir=output_dir,
+ )
+ else:
+ print(f"Processing as mesh: {input_file}")
+ _install_blender()
+ results = model.get_mesh_materials(
+ input_file,
+ blender_path=BLENDER_PATH,
+ query_points="voxel_centers",
+ output_dir=output_dir,
+ return_original_scale=True,
+ )
+
+ save_materials(results, material_file)
+ print(f"Materials saved to: {material_file}")
+
+ all_images = _get_render_images(output_dir)
+ first_image = all_images[0] if all_images else None
+
+ visualizations = _create_material_visualizations(material_file, output_dir)
+
+ youngs_views = visualizations.get("youngs_modulus", ([], None))[0]
+ youngs_colorbar = visualizations.get("youngs_modulus", ([], None))[1]
+ youngs_first = youngs_views[0] if youngs_views else None
+
+ poissons_views = visualizations.get("poissons_ratio", ([], None))[0]
+ poissons_colorbar = visualizations.get("poissons_ratio", ([], None))[1]
+ poissons_first = poissons_views[0] if poissons_views else None
+
+ density_views = visualizations.get("density", ([], None))[0]
+ density_colorbar = visualizations.get("density", ([], None))[1]
+ density_first = density_views[0] if density_views else None
+
+ return (
+ first_image,
+ all_images,
+ youngs_first,
+ youngs_views,
+ youngs_colorbar,
+ poissons_first,
+ poissons_views,
+ poissons_colorbar,
+ density_first,
+ density_views,
+ density_colorbar,
+ material_file,
+ )
+
+ except Exception as e:
+ print(f"Error processing 3D model: {e}")
+ raise gr.Error(f"Failed to process 3D model: {str(e)}")
+
+
+def update_slider_image(slider_value: int, all_images: List[str]) -> Optional[str]:
+ if not all_images or slider_value < 0 or slider_value >= len(all_images):
+ return None
+ return all_images[slider_value]
+
+
+def update_property_view(slider_value: int, views: List[str]) -> Optional[str]:
+ if not views or slider_value < 0 or slider_value >= len(views):
+ return None
+ return views[slider_value]
+
+
+css = """
+.gradio-container {
+ font-family: 'IBM Plex Sans', sans-serif;
+}
+
+.title-container {
+ text-align: center;
+ padding: 20px 0;
+}
+
+.badge-container {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 20px;
+}
+
+.badge-container a img {
+ height: 22px;
+}
+
+h1 {
+ text-align: center;
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ text-align: center;
+ color: #666;
+ font-size: 1.1rem;
+ margin-bottom: 1.5rem;
+}
+
+.input-column, .output-column {
+ min-height: 400px;
+}
+
+.output-column .row {
+ display: flex !important;
+ flex-wrap: nowrap !important;
+ gap: 16px;
+}
+
+.output-column .row > .column {
+ flex: 1 1 50% !important;
+ min-width: 0 !important;
+}
+"""
+
+title_md = """
+
+
VoMP: Predicting Volumetric Mechanical Properties
+
Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, and more.
+
+
+"""
+
+description_md = """
+Upload a Gaussian Splat (.ply) or Mesh (.obj, .glb, .stl, .gltf) to predict volumetric mechanical properties (Young's modulus, Poisson's ratio, density) for realistic physics simulation.
+"""
+
+with gr.Blocks(css=css, title="VoMP") as demo:
+ all_images_state = gr.State([])
+ youngs_views_state = gr.State([])
+ poissons_views_state = gr.State([])
+ density_views_state = gr.State([])
+
+ gr.HTML(title_md)
+ gr.Markdown(description_md)
+
+ with gr.Row():
+ # Input Column (50%)
+ with gr.Column(scale=1, elem_classes="input-column"):
+ gr.Markdown("### ๐ค Input")
+ input_model = gr.Model3D(
+ label="Upload 3D Model",
+ clear_color=[0.1, 0.1, 0.1, 1.0],
+ )
+
+ submit_btn = gr.Button(
+ "๐ Generate Materials", variant="primary", size="lg"
+ )
+
+ gr.Markdown("#### ๐ฌ Rendered Views")
+ rendered_image = gr.Image(label="Rendered View", height=250)
+
+ view_slider = gr.Slider(
+ minimum=0,
+ maximum=NUM_VIEWS - 1,
+ step=1,
+ value=0,
+ label="Browse All Views",
+ info=f"Slide to view all {NUM_VIEWS} rendered views",
+ )
+
+ # Output Column (50%)
+ with gr.Column(scale=1, elem_classes="output-column"):
+ gr.Markdown("### ๐ฅ Output - Material Properties")
+
+ # Row 1: Young's Modulus and Poisson's Ratio
+ with gr.Row():
+ with gr.Column(scale=1, min_width=200):
+ youngs_image = gr.Image(label="Young's Modulus", height=200)
+ youngs_slider = gr.Slider(
+ minimum=0,
+ maximum=2,
+ step=1,
+ value=0,
+ label="View",
+ info="Switch between 3 views",
+ )
+ youngs_colorbar = gr.Image(height=50, show_label=False)
+
+ with gr.Column(scale=1, min_width=200):
+ poissons_image = gr.Image(label="Poisson's Ratio", height=200)
+ poissons_slider = gr.Slider(
+ minimum=0,
+ maximum=2,
+ step=1,
+ value=0,
+ label="View",
+ info="Switch between 3 views",
+ )
+ poissons_colorbar = gr.Image(height=50, show_label=False)
+
+ # Row 2: Density and Download
+ with gr.Row():
+ with gr.Column(scale=1, min_width=200):
+ density_image = gr.Image(label="Density", height=200)
+ density_slider = gr.Slider(
+ minimum=0,
+ maximum=2,
+ step=1,
+ value=0,
+ label="View",
+ info="Switch between 3 views",
+ )
+ density_colorbar = gr.Image(height=50, show_label=False)
+
+ with gr.Column(scale=1, min_width=200):
+ gr.Markdown("#### ๐พ Download")
+ output_file = gr.File(
+ label="Download Materials (.npz)",
+ file_count="single",
+ )
+
+ gr.Markdown("### ๐ฏ Examples")
+ gr.Examples(
+ examples=[
+ [os.path.join(EXAMPLES_DIR, "dog.ply")],
+ [os.path.join(EXAMPLES_DIR, "dozer.ply")],
+ [os.path.join(EXAMPLES_DIR, "fiscus.ply")],
+ [os.path.join(EXAMPLES_DIR, "plant.ply")],
+ ],
+ inputs=[input_model],
+ outputs=[
+ rendered_image,
+ all_images_state,
+ youngs_image,
+ youngs_views_state,
+ youngs_colorbar,
+ poissons_image,
+ poissons_views_state,
+ poissons_colorbar,
+ density_image,
+ density_views_state,
+ density_colorbar,
+ output_file,
+ ],
+ fn=process_3d_model,
+ cache_examples=False,
+ )
+
+ # Event handlers
+ submit_btn.click(
+ fn=process_3d_model,
+ inputs=[input_model],
+ outputs=[
+ rendered_image,
+ all_images_state,
+ youngs_image,
+ youngs_views_state,
+ youngs_colorbar,
+ poissons_image,
+ poissons_views_state,
+ poissons_colorbar,
+ density_image,
+ density_views_state,
+ density_colorbar,
+ output_file,
+ ],
+ )
+
+ view_slider.change(
+ fn=update_slider_image,
+ inputs=[view_slider, all_images_state],
+ outputs=[rendered_image],
+ )
+
+ youngs_slider.change(
+ fn=update_property_view,
+ inputs=[youngs_slider, youngs_views_state],
+ outputs=[youngs_image],
+ )
+
+ poissons_slider.change(
+ fn=update_property_view,
+ inputs=[poissons_slider, poissons_views_state],
+ outputs=[poissons_image],
+ )
+
+ density_slider.change(
+ fn=update_property_view,
+ inputs=[density_slider, density_views_state],
+ outputs=[density_image],
+ )
+
+if __name__ == "__main__":
+ demo.launch()
diff --git a/deps/vomp/gradio/examples/dog.ply b/deps/vomp/gradio/examples/dog.ply
new file mode 100644
index 0000000000000000000000000000000000000000..3e1633c14314d56760ec0070addca8d558602d5d
--- /dev/null
+++ b/deps/vomp/gradio/examples/dog.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1808d618588f67a083bca52848070297be63d24ba3ddf1e9dae7edd75d87c69e
+size 2477313
diff --git a/deps/vomp/gradio/examples/dozer.ply b/deps/vomp/gradio/examples/dozer.ply
new file mode 100644
index 0000000000000000000000000000000000000000..123bce7fe994313d69468e1f1dada40bc1534c78
--- /dev/null
+++ b/deps/vomp/gradio/examples/dozer.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b44ab835c41c4b4e2ad4b55456de1173f38e8bcef005de4d5d9d2c27f7b3749e
+size 84598795
diff --git a/deps/vomp/gradio/examples/fiscus.ply b/deps/vomp/gradio/examples/fiscus.ply
new file mode 100644
index 0000000000000000000000000000000000000000..b70b72e987a65ffc307d12961957bb1c2d5f9ab3
--- /dev/null
+++ b/deps/vomp/gradio/examples/fiscus.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7516d90f1ec0764ce653819057b1b69197a56bc31683b4a7fd4fdff247437f7e
+size 74925059
diff --git a/deps/vomp/gradio/examples/plant.ply b/deps/vomp/gradio/examples/plant.ply
new file mode 100644
index 0000000000000000000000000000000000000000..daa7f5ca407d0b5fcd3e220e81a867a3d574aec4
--- /dev/null
+++ b/deps/vomp/gradio/examples/plant.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3a659f85e78556a90cddb2f37fcf77e9bd452fda15de6f69f6db8c5803bf9fd5
+size 12734620
diff --git a/deps/vomp/gradio/requirements.txt b/deps/vomp/gradio/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8b07368cec76ee7d9532753983f5d5506dab25a6
--- /dev/null
+++ b/deps/vomp/gradio/requirements.txt
@@ -0,0 +1,23 @@
+gradio
+-e git+https://github.com/nv-tlabs/vomp.git#egg=vomp
+polyscope==2.5.0
+typing==3.7.4.3
+trimesh==4.8.1
+Pillow==11.0.0
+safetensors==0.6.2
+easydict==1.13
+scipy==1.14.1
+git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
+pyparsing==3.2.3
+opencv-python-headless==4.10.0.84
+numpy==1.26.4
+matplotlib==3.7.5
+torch==2.4.0
+torch-tensorrt==2.4.0
+torchvision==0.19.0
+xformers==0.0.27.post2
+git+https://github.com/graphdeco-inria/diff-gaussian-rasterization.git@59f5f77e3ddbac3ed9db93ec2cfe99ed6c5d121d
+https://github.com/Dao-AILab/flash-attention/releases/download/v2.5.9.post1/flash_attn-2.5.9.post1+cu118torch2.4cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
+spconv-cu120
+--find-links https://nvidia-kaolin.s3.us-east-2.amazonaws.com/torch-2.4.0_cu121.html
+kaolin==0.18.0
\ No newline at end of file
diff --git a/deps/vomp/install_env.sh b/deps/vomp/install_env.sh
new file mode 100755
index 0000000000000000000000000000000000000000..39592f233de332bf17c4a72a7576e6190eb47fc1
--- /dev/null
+++ b/deps/vomp/install_env.sh
@@ -0,0 +1,311 @@
+#!/bin/bash -e
+
+set -o nounset
+set -o pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd "$DIR"
+
+CONDA_ENV=${1:-"vomp"}
+CUDA_VERSION=${CUDA_VERSION:-"11.8.0"}
+
+export CLI_COLOR=1
+RED='\033[1;31m'
+GREEN='\033[1;32m'
+NOCOLOR='\033[0m'
+
+USAGE="${GREEN}Usage: bash $0 [CONDA_ENV] [AUTO_CONFIRM]${NOCOLOR}
+\n\n
+Arguments:
+ CONDA_ENV : Name of conda environment (default: vomp)
+ AUTO_CONFIRM : YES to skip all confirmation prompts, NO to ask for each (default: NO)
+\n\n
+Examples:
+ bash $0 # Use default env name, ask for confirmations
+ bash $0 my_env # Use 'my_env' as environment name, ask for confirmations
+ bash $0 vomp YES # Use default env name, auto-confirm all prompts
+\n\n
+"
+
+if [ $# -gt "2" ]; then
+ echo -e "${RED}Error:${NOCOLOR} Wrong argument number"
+ echo
+ echo -e "$USAGE"
+ exit 1
+fi
+
+AUTOCONFIRM="NO"
+if [ $# -ge 2 ]; then
+ if [ "$2" == "YES" ]; then
+ AUTOCONFIRM="YES"
+ fi
+fi
+
+if [ "$AUTOCONFIRM" == "YES" ]; then
+ echo -e "Auto-confirm: ${RED}ENABLED${NOCOLOR} - script will not ask for confirmation"
+ echo ""
+else
+ echo -e "Auto-confirm: ${GREEN}DISABLED${NOCOLOR} - script will ask for confirmation"
+ echo ""
+fi
+
+ask_yes_no() {
+ local prompt="$1"
+ local answer=""
+
+ if [ "$AUTOCONFIRM" == "YES" ]; then
+ echo ""
+ echo -e "${GREEN}$prompt${NOCOLOR} (Y/N): ${GREEN}Y (auto-confirmed)${NOCOLOR}"
+ return 0
+ else
+ echo ""
+ while true; do
+ echo -ne "${GREEN}$prompt${NOCOLOR} (Y/N): "
+ read answer
+ case $answer in
+ [Yy]* ) echo -e "${GREEN}Y${NOCOLOR}"; return 0;;
+ [Nn]* ) echo -e "${RED}N${NOCOLOR}"; return 1;;
+ * ) echo -e "${RED}Please answer Y or N.${NOCOLOR}";;
+ esac
+ done
+ fi
+}
+
+echo "--------------------------------------------------------------------"
+echo -e "${GREEN}Installing System-Wide Dependencies${NOCOLOR}"
+echo "--------------------------------------------------------------------"
+
+if ask_yes_no "Ok to install OpenGL (libglib2.0-dev libgl) required only if you use material visualization GUI?"; then
+ echo -e "${GREEN}...Running OpenGL installation${NOCOLOR}"
+ sudo apt-get install -y libglib2.0-dev libgl || { echo -e "${RED}Failed to install OpenGL dependencies${NOCOLOR}"; exit 1; }
+ conda install -c conda-forge mesa-libgl-devel-cos7-x86_64 -y || { echo -e "${RED}Failed to install mesa-libgl${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}...Skipping OpenGL installation${NOCOLOR}"
+fi
+
+if ask_yes_no "Ok to install EGL (libegl1) required only if you use material visualization GUI without a display?"; then
+ echo -e "${GREEN}...Running EGL installation${NOCOLOR}"
+ sudo apt-get install -y libegl1 || { echo -e "${RED}Failed to install EGL${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}...Skipping EGL installation${NOCOLOR}"
+fi
+
+# Create and activate conda environment
+eval "$(conda shell.bash hook)"
+
+# Finds the path of the environment if the environment already exists
+CONDA_ENV_PATH=$(conda env list | sed -E -n "s/^${CONDA_ENV}[[:space:]]+\*?[[:space:]]*(.*)$/\1/p")
+if [ -z "${CONDA_ENV_PATH}" ]; then
+ if ask_yes_no "Conda environment '${CONDA_ENV}' not found. Create new environment with Python 3.10?"; then
+ echo -e "${GREEN}Creating conda environment '${CONDA_ENV}'${NOCOLOR}"
+ conda create --name "${CONDA_ENV}" -y python=3.10 || { echo -e "${RED}Failed to create conda environment${NOCOLOR}"; exit 1; }
+ else
+ echo -e "${RED}Environment creation cancelled. Exiting.${NOCOLOR}"
+ exit 1
+ fi
+else
+ echo -e "${GREEN}NOTE: Conda environment '${CONDA_ENV}' already exists at ${CONDA_ENV_PATH}${NOCOLOR}"
+ if ask_yes_no "Ok to install packages in existing conda environment '${CONDA_ENV}'?"; then
+ echo -e "${GREEN}Proceeding with installation in existing environment${NOCOLOR}"
+ else
+ echo -e "${RED}Installation cancelled. Exiting.${NOCOLOR}"
+ exit 1
+ fi
+fi
+conda activate "$CONDA_ENV"
+
+if ask_yes_no "Install/reinstall NVIDIA CUDA ${CUDA_VERSION} in conda environment? (Skip if already installed)"; then
+ echo -e "${GREEN}...Installing NVIDIA Cuda (in Conda env; keeping systems settings intact)${NOCOLOR}"
+ conda install -y cuda="${CUDA_VERSION}" -c nvidia/label/cuda-"${CUDA_VERSION}" || { echo -e "${RED}Failed to install CUDA${NOCOLOR}"; exit 1; }
+ conda install -y cmake ninja cuda-toolkit -c nvidia/label/cuda-"${CUDA_VERSION}" || { echo -e "${RED}Failed to install build tools${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}...Skipping CUDA installation${NOCOLOR}"
+fi
+
+echo -e "${GREEN}...Setting up CUDA environment${NOCOLOR}"
+export CUDA_HOME=$CONDA_PREFIX
+export PATH=$CUDA_HOME/bin:$PATH
+export LD_LIBRARY_PATH=$CUDA_HOME/lib64:${LD_LIBRARY_PATH:-}
+conda env config vars set CUDA_HOME="$CUDA_HOME"
+conda env config vars set PATH="$PATH"
+conda env config vars set LD_LIBRARY_PATH="$LD_LIBRARY_PATH"
+
+echo "--------------------------------------------------------------------"
+echo -e "${GREEN}Installing Pip/Conda Packages${NOCOLOR}"
+echo "--------------------------------------------------------------------"
+
+echo -e "${GREEN}...Installing utilities${NOCOLOR}"
+pip install --upgrade pip wheel setuptools pytest || { echo -e "${RED}Failed to install pip utilities${NOCOLOR}"; exit 1; }
+
+if ask_yes_no "Install/reinstall torch-tensorrt? (Skip if already installed)"; then
+ echo -e "${GREEN}...Installing torch-tensorrt${NOCOLOR}"
+ if [ "$CUDA_VERSION" == "11.8.0" ]; then
+ pip install -U torch-tensorrt==2.4.0 --no-deps --index-url https://download.pytorch.org/whl/cu118 || { echo -e "${RED}Failed to install torch-tensorrt${NOCOLOR}"; exit 1; }
+ else
+ pip install -U torch-tensorrt==2.4.0 --no-deps || { echo -e "${RED}Failed to install torch-tensorrt${NOCOLOR}"; exit 1; }
+ fi
+else
+ echo -e "${RED}...Skipping torch-tensorrt installation${NOCOLOR}"
+fi
+
+if ask_yes_no "Install/reinstall PyTorch 2.4.0 and related packages? (Skip if already installed with correct version)"; then
+ echo -e "${GREEN}...Installing PyTorch${NOCOLOR}"
+ if [ "$CUDA_VERSION" == "11.8.0" ]; then
+ pip install torch==2.4.0 torchvision==0.19.0 xformers==0.0.27.post2 --index-url https://download.pytorch.org/whl/cu118 || { echo -e "${RED}Failed to install PyTorch${NOCOLOR}"; exit 1; }
+ else
+ pip install torch==2.4.0 torchvision==0.19.0 xformers==0.0.27.post2 || { echo -e "${RED}Failed to install PyTorch${NOCOLOR}"; exit 1; }
+ fi
+else
+ echo -e "${RED}...Skipping PyTorch installation${NOCOLOR}"
+fi
+
+if ask_yes_no "Install/reinstall diff-gaussian-rasterization? (Skip if already installed)"; then
+ echo -e "${GREEN}...Installing diff-gaussian-rasterization${NOCOLOR}"
+ pip install git+https://github.com/graphdeco-inria/diff-gaussian-rasterization.git@59f5f77e3ddbac3ed9db93ec2cfe99ed6c5d121d --no-build-isolation || { echo -e "${RED}Failed to install diff-gaussian-rasterization${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}...Skipping diff-gaussian-rasterization installation${NOCOLOR}"
+fi
+
+
+echo -e "${GREEN}...Installing flash_attn${NOCOLOR}"
+if ask_yes_no "Install prebuilt flash_attn binary (faster) instead of building from source?"; then
+ echo -e "${GREEN}Installing prebuilt flash_attn binary${NOCOLOR}"
+ pip install https://github.com/Dao-AILab/flash-attention/releases/download/v2.5.9.post1/flash_attn-2.5.9.post1+cu118torch2.4cxx11abiFALSE-cp310-cp310-linux_x86_64.whl --no-build-isolation || { echo -e "${RED}Failed to install prebuilt flash_attn${NOCOLOR}"; exit 1; }
+else
+ echo -e "${GREEN}Building flash_attn from source${NOCOLOR}"
+ pip install flash_attn==2.5.9.post1 --no-build-isolation || { echo -e "${RED}Failed to build flash_attn from source${NOCOLOR}"; exit 1; }
+fi
+
+if ask_yes_no "Install/reinstall spconv? (Skip if already installed with correct CUDA version)"; then
+ if [[ "$CUDA_VERSION" == 11.8* ]] || [[ "$CUDA_VERSION" == 11* ]]; then
+ echo -e "${GREEN}Installing spconv for CUDA 11.x${NOCOLOR}"
+ pip install spconv-cu118 || { echo -e "${RED}Failed to install spconv-cu118${NOCOLOR}"; exit 1; }
+ elif [[ "$CUDA_VERSION" == 12.0* ]] || [[ "$CUDA_VERSION" == 12* ]]; then
+ echo -e "${GREEN}Installing spconv for CUDA 12.x${NOCOLOR}"
+ pip install spconv-cu120 || { echo -e "${RED}Failed to install spconv-cu120${NOCOLOR}"; exit 1; }
+ else
+ echo -e "${RED}Warning: No prebuilt spconv wheel for CUDA version $CUDA_VERSION. Please install spconv manually.${NOCOLOR}"
+ fi
+else
+ echo -e "${RED}...Skipping spconv installation${NOCOLOR}"
+fi
+
+if ask_yes_no "Install/reinstall Kaolin 0.18.0? (Skip if already installed with correct CUDA version)"; then
+ echo -e "${GREEN}...Installing Kaolin${NOCOLOR}"
+ # Extract CUDA version digits (e.g., 11.8.0 -> 118, 12.0.1 -> 120)
+ KAOLIN_CUDA_VER=$(echo "$CUDA_VERSION" | awk -F. '{printf "%s%s", $1, $2}')
+ pip install kaolin==0.18.0 -f https://nvidia-kaolin.s3.us-east-2.amazonaws.com/torch-2.4.0_cu"${KAOLIN_CUDA_VER}".html || { echo -e "${RED}Failed to install Kaolin${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}...Skipping Kaolin installation${NOCOLOR}"
+fi
+
+if ask_yes_no "Install base inference requirements from requirements_inference.txt?"; then
+ echo -e "${GREEN}Installing from requirements_inference.txt${NOCOLOR}"
+ pip install -r requirements_inference.txt || { echo -e "${RED}Failed to install from requirements_inference.txt${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}Skipping base inference requirements${NOCOLOR}"
+fi
+
+if ask_yes_no "Install dev requirements from requirements_dev.txt (only required for training or finetuning)?"; then
+ echo -e "${GREEN}Installing from requirements_dev.txt${NOCOLOR}"
+ pip install -r requirements_dev.txt || { echo -e "${RED}Failed to install from requirements_dev.txt${NOCOLOR}"; exit 1; }
+else
+ echo -e "${RED}Skipping dev requirements${NOCOLOR}"
+fi
+
+echo "--------------------------------------------------------------------"
+echo -e "${GREEN}Setting Up Blender${NOCOLOR}"
+echo "--------------------------------------------------------------------"
+
+BLENDER_PATH="./blender-3.0.1-linux-x64/blender"
+
+# Check if Blender is already installed
+if [ -f "$BLENDER_PATH" ]; then
+ echo -e "${GREEN}Blender already found at $BLENDER_PATH${NOCOLOR}"
+ echo -e "${GREEN}Setting BLENDER_BIN environment variable...${NOCOLOR}"
+ conda env config vars set BLENDER_BIN="$BLENDER_PATH"
+ echo -e "${GREEN}Blender path configured successfully!${NOCOLOR}"
+elif ask_yes_no "Auto-install Blender 3.0.1 for material visualization? (Required for mesh material estimation)"; then
+ echo -e "${GREEN}Installing Blender system dependencies...${NOCOLOR}"
+ if ask_yes_no "Ok to update apt-get?"; then
+ sudo apt-get update || { echo -e "${RED}Failed to update apt-get${NOCOLOR}"; exit 1; }
+ else
+ echo -e "${RED}...Skipping apt-get update${NOCOLOR}"
+ echo "You can update them manually with: sudo apt-get update"
+ fi
+
+ if ask_yes_no "Ok to install system-wide dependencies for Blender (libxrender1 libxi6 libxkbcommon-x11-0 libsm6)?"; then
+ sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6 || { echo -e "${RED}Failed to install Blender dependencies${NOCOLOR}"; exit 1; }
+ else
+ echo -e "${RED}...Skipping Blender system dependencies${NOCOLOR}"
+ echo "You can install them manually with: sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
+ fi
+
+ echo -e "${GREEN}Downloading Blender 3.0.1...${NOCOLOR}"
+ if [ ! -f "blender-3.0.1-linux-x64.tar.xz" ]; then
+ wget https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz || { echo -e "${RED}Failed to download Blender${NOCOLOR}"; exit 1; }
+ else
+ echo -e "${GREEN}Blender archive already downloaded.${NOCOLOR}"
+ fi
+
+ echo -e "${GREEN}Extracting Blender...${NOCOLOR}"
+ if [ ! -d "blender-3.0.1-linux-x64" ]; then
+ tar -xf blender-3.0.1-linux-x64.tar.xz || { echo -e "${RED}Failed to extract Blender${NOCOLOR}"; exit 1; }
+ else
+ echo -e "${GREEN}Blender already extracted.${NOCOLOR}"
+ fi
+
+ if [ -f "$BLENDER_PATH" ]; then
+ echo -e "${GREEN}Setting BLENDER_BIN environment variable to: $BLENDER_PATH${NOCOLOR}"
+ conda env config vars set BLENDER_BIN="$BLENDER_PATH"
+ echo -e "${GREEN}Blender installed and configured successfully!${NOCOLOR}"
+ echo -e "${GREEN}Note: You may need to reactivate the conda environment for the change to take effect.${NOCOLOR}"
+ else
+ echo -e "${RED}Error: Blender installation failed!${NOCOLOR}"
+ exit 1
+ fi
+elif ask_yes_no "Configure custom Blender path instead?"; then
+ echo "Default Blender path: $BLENDER_PATH"
+ read -p "Enter Blender executable path (or press Enter for default): " CUSTOM_BLENDER_PATH
+
+ if [ ! -z "$CUSTOM_BLENDER_PATH" ]; then
+ BLENDER_PATH="$CUSTOM_BLENDER_PATH"
+ fi
+
+ if [ -f "$BLENDER_PATH" ]; then
+ echo -e "${GREEN}Setting BLENDER_BIN environment variable to: $BLENDER_PATH${NOCOLOR}"
+ conda env config vars set BLENDER_BIN="$BLENDER_PATH"
+ echo -e "${GREEN}Blender path configured successfully!${NOCOLOR}"
+ echo -e "${GREEN}Note: You may need to reactivate the conda environment for the change to take effect.${NOCOLOR}"
+ else
+ echo -e "${RED}Warning: Blender executable not found at $BLENDER_PATH${NOCOLOR}"
+ echo "You can manually set it later with: conda env config vars set BLENDER_BIN="
+ fi
+else
+ echo -e "${RED}Skipping Blender setup.${NOCOLOR} You can install it later and set the path with:"
+ echo " conda env config vars set BLENDER_BIN="
+fi
+
+echo "--------------------------------------------------------------------"
+echo -e "${GREEN}Installing VoMP${NOCOLOR}"
+echo "--------------------------------------------------------------------"
+
+echo -e "${GREEN}Installing vox2seq extension${NOCOLOR}"
+pip install extensions/vox2seq/ --no-build-isolation || { echo -e "${RED}Failed to install vox2seq${NOCOLOR}"; exit 1; }
+echo -e "${GREEN}Installing VoMP in editable mode${NOCOLOR}"
+pip install -e . || { echo -e "${RED}Failed to install VoMP${NOCOLOR}"; exit 1; }
+
+echo "--------------------------------------------------------------------"
+echo -e "${GREEN}Downloading Model Weights${NOCOLOR}"
+echo "--------------------------------------------------------------------"
+
+echo -e "If you just want to use the model for inference, follow the instructions in the README.md file to download the weights."
+
+if ask_yes_no "Download initialization weights? (for training from scratch)"; then
+ echo -e "${GREEN}Downloading initialization weights${NOCOLOR}"
+ chmod +x weights/download.sh
+ ./weights/download.sh || { echo -e "${RED}Failed to download initialization weights${NOCOLOR}"; exit 1; }
+ echo -e "${GREEN}Initialization weights downloaded successfully${NOCOLOR}"
+else
+ echo -e "${RED}Skipping initialization weights download${NOCOLOR}"
+fi
\ No newline at end of file
diff --git a/deps/vomp/pyproject.toml b/deps/vomp/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..3712584bc04940f561e897d0af9a0386f64eb936
--- /dev/null
+++ b/deps/vomp/pyproject.toml
@@ -0,0 +1,18 @@
+[build-system]
+requires = ["setuptools>=68", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "vomp"
+version = "0.1.0"
+description = "VoMP: Predicting Volumetric Mechanical Properties"
+readme = "README.md"
+requires-python = ">=3.10"
+license = { text = "Apache-2.0" }
+
+[tool.setuptools]
+include-package-data = true
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["vomp*", "dataset_toolkits*"]
diff --git a/deps/vomp/requirements_dev.txt b/deps/vomp/requirements_dev.txt
new file mode 100644
index 0000000000000000000000000000000000000000..34bd1b2a23180c52d7f53a728190a0d341ccebc7
--- /dev/null
+++ b/deps/vomp/requirements_dev.txt
@@ -0,0 +1,23 @@
+polyscope==2.5.0
+typing==3.7.4.3
+trimesh==4.8.1
+Pillow==11.0.0
+safetensors==0.6.2
+easydict==1.13
+scipy==1.14.1
+git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
+git+https://github.com/NVlabs/nvdiffrast.git@v0.3.3
+pyparsing==3.2.3
+opencv-python-headless==4.10.0.84
+pandas==2.2.3
+scikit-learn==1.5.2
+huggingface-hub==0.34.4
+qwen-vl-utils==0.0.11
+transformers==4.51.3
+accelerate==1.10.1
+openai==1.106.1
+tensorboard==2.20.0
+open3d==0.18.0
+lpips==0.1.4
+numpy==1.26.4
+matplotlib==3.7.5
\ No newline at end of file
diff --git a/deps/vomp/requirements_full.txt b/deps/vomp/requirements_full.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bb1dc0735d80c4b2808e803dc0f554eb98ea23f8
--- /dev/null
+++ b/deps/vomp/requirements_full.txt
@@ -0,0 +1,174 @@
+absl-py==2.3.1
+accelerate==1.10.1
+addict==2.4.0
+annotated-types==0.7.0
+anyio==4.11.0
+asttokens==3.0.0
+attrs==25.4.0
+av==16.0.1
+blinker==1.9.0
+ccimport==0.4.4
+certifi==2025.10.5
+charset-normalizer==3.4.4
+click==8.3.0
+comm==0.2.3
+ConfigArgParse==1.7.1
+contourpy==1.3.2
+cumm-cu118==0.7.11
+cycler==0.12.1
+Cython==3.1.2
+dash==3.2.0
+dataclasses-json==0.6.7
+decorator==5.2.1
+decord==0.6.0
+Deprecated==1.3.1
+diff_gaussian_rasterization @ git+https://github.com/graphdeco-inria/diff-gaussian-rasterization.git@59f5f77e3ddbac3ed9db93ec2cfe99ed6c5d121d
+dill==0.4.0
+distro==1.9.0
+easydict==1.13
+einops==0.8.1
+entrypoints==0.4
+exceptiongroup==1.3.0
+executing==2.2.1
+fastjsonschema==2.21.2
+filelock==3.18.0
+fire==0.7.1
+flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.5.9.post1/flash_attn-2.5.9.post1+cu118torch2.4cxx11abiFALSE-cp310-cp310-linux_x86_64.whl#sha256=0b6ddd4acf99938100ca53764c98b839364f4473efddadc057649dcbbe9cc236
+Flask==3.1.2
+fonttools==4.59.0
+fsspec==2025.3.2
+git-filter-repo==2.47.0
+glcontext==3.0.0
+grpcio==1.76.0
+h11==0.16.0
+hf-xet==1.2.0
+httpcore==1.0.9
+httpx==0.28.1
+huggingface-hub==0.34.4
+idna==3.11
+imageio==2.37.0
+imageio-ffmpeg==0.6.0
+importlib_metadata==8.7.0
+iniconfig==2.1.0
+ipycanvas==0.14.1
+ipyevents==2.0.4
+ipython==8.37.0
+ipywidgets==8.1.8
+itsdangerous==2.2.0
+jedi==0.19.2
+Jinja2==3.1.6
+jiter==0.11.1
+joblib==1.5.2
+jsonschema==4.25.1
+jsonschema-specifications==2025.9.1
+jupyter_client==7.4.9
+jupyter_core==5.9.1
+jupyterlab_widgets==3.0.16
+kaolin==0.18.0
+kiwisolver==1.4.8
+lark==1.3.1
+lpips==0.1.4
+Markdown==3.10
+MarkupSafe==2.1.5
+marshmallow==3.26.1
+matplotlib==3.7.5
+matplotlib-inline==0.2.1
+moderngl==5.12.0
+mpmath==1.3.0
+mypy_extensions==1.1.0
+narwhals==2.10.2
+nbformat==5.10.4
+nest-asyncio==1.6.0
+networkx==3.3
+ninja==1.13.0
+numpy==1.26.4
+nvdiffrast @ git+https://github.com/NVlabs/nvdiffrast.git@729261dc64c4241ea36efda84fbf532cc8b425b8
+nvidia-cublas-cu11==11.11.3.6
+nvidia-cuda-cupti-cu11==11.8.87
+nvidia-cuda-nvrtc-cu11==11.8.89
+nvidia-cuda-runtime-cu11==11.8.89
+nvidia-cudnn-cu11==9.1.0.70
+nvidia-cufft-cu11==10.9.0.58
+nvidia-curand-cu11==10.3.0.86
+nvidia-cusolver-cu11==11.4.1.48
+nvidia-cusparse-cu11==11.7.5.86
+nvidia-nccl-cu11==2.20.5
+nvidia-nvtx-cu11==11.8.86
+open3d==0.18.0
+openai==1.106.1
+opencv-python-headless==4.10.0.84
+packaging==25.0
+pandas==2.2.3
+parso==0.8.5
+pccm==0.4.16
+pexpect==4.9.0
+pillow==11.0.0
+platformdirs==4.5.0
+plotly==6.4.0
+pluggy==1.6.0
+polyscope==2.5.0
+portalocker==3.2.0
+prompt_toolkit==3.0.52
+protobuf==6.33.0
+psutil==7.1.3
+ptyprocess==0.7.0
+pure_eval==0.2.3
+pybind11==3.0.1
+pybullet==3.2.7
+pydantic==2.12.4
+pydantic_core==2.41.5
+pygltflib==1.16.5
+Pygments==2.19.2
+pyparsing==3.2.3
+pyquaternion==0.9.9
+pytest==8.4.2
+python-dateutil==2.9.0.post0
+pytz==2025.2
+PyYAML==6.0.3
+pyzmq==27.1.0
+qwen-vl-utils==0.0.11
+referencing==0.37.0
+regex==2024.11.6
+requests==2.32.5
+retrying==1.4.2
+rpds-py==0.28.0
+safetensors==0.6.2
+scikit-learn==1.5.2
+scipy==1.14.1
+six==1.17.0
+sniffio==1.3.1
+spconv-cu118==2.3.8
+stack-data==0.6.3
+sympy==1.14.0
+tensorboard==2.20.0
+tensorboard-data-server==0.7.2
+termcolor==3.2.0
+threadpoolctl==3.6.0
+tokenizers==0.21.4
+tomli==2.2.1
+torch==2.4.0+cu118
+torch_tensorrt==2.4.0+cu118
+torchvision==0.19.0+cu118
+tornado==6.5.2
+tqdm==4.67.1
+traitlets==5.14.3
+transformers==4.51.3
+trimesh==4.8.1
+triton==3.0.0
+typing==3.7.4.3
+typing-inspect==0.9.0
+typing-inspection==0.4.2
+typing_extensions==4.15.0
+tzdata==2025.2
+urllib3==2.5.0
+usd-core==25.5
+utils3d @ git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
+vox2seq @ file:///home/rdagli/code/vomp/extensions/vox2seq
+warp-lang==1.10.0
+wcwidth==0.2.14
+Werkzeug==3.1.3
+widgetsnbextension==4.0.15
+wrapt==2.0.0
+xformers==0.0.27.post2+cu118
+zipp==3.23.0
+zstandard==0.23.0
diff --git a/deps/vomp/requirements_inference.txt b/deps/vomp/requirements_inference.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4ab11c0fa85cd5d6cd946fc86f760a91e4b9122c
--- /dev/null
+++ b/deps/vomp/requirements_inference.txt
@@ -0,0 +1,13 @@
+polyscope==2.5.0
+typing==3.7.4.3
+trimesh==4.8.1
+Pillow==11.0.0
+safetensors==0.6.2
+easydict==1.13
+scipy==1.14.1
+git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
+git+https://github.com/NVlabs/nvdiffrast.git@v0.3.3
+pyparsing==3.2.3
+opencv-python-headless==4.10.0.84
+numpy==1.26.4
+matplotlib==3.7.5
\ No newline at end of file
diff --git a/deps/vomp/scripts/data_stats.py b/deps/vomp/scripts/data_stats.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea0e3d00f63f03e67a2fe053748f47b5c4c427a7
--- /dev/null
+++ b/deps/vomp/scripts/data_stats.py
@@ -0,0 +1,526 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pandas as pd
+import numpy as np
+import os
+import json
+import sys
+from pathlib import Path
+from collections import defaultdict, Counter
+
+# Add paths to import material annotations
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+
+def load_material_annotations():
+ """Load material annotations JSON file"""
+ json_path = "datasets/raw/material_annotations.json"
+ if os.path.exists(json_path):
+ with open(json_path, "r") as f:
+ return json.load(f)
+ return []
+
+
+def get_material_properties_stats(segments):
+ """Extract material properties for statistical analysis"""
+ densities = []
+ youngs_moduli = []
+ poissons_ratios = []
+
+ for segment_name, segment_data in segments.items():
+ density = segment_data.get("density")
+ youngs_modulus = segment_data.get("youngs_modulus")
+ poissons_ratio = segment_data.get("poissons_ratio")
+
+ if density is not None:
+ densities.append(density)
+ if youngs_modulus is not None:
+ youngs_moduli.append(youngs_modulus)
+ if poissons_ratio is not None:
+ poissons_ratios.append(poissons_ratio)
+
+ return densities, youngs_moduli, poissons_ratios
+
+
+def print_property_stats(values, property_name, unit=""):
+ """Print comprehensive statistics for a material property"""
+ if not values:
+ print(f" {property_name}: No data available")
+ return
+
+ values = np.array(values)
+ print(f" {property_name} ({unit}):")
+ print(f" Count: {len(values)}")
+ print(f" Min: {np.min(values):.6e}")
+ print(f" Max: {np.max(values):.6e}")
+ print(f" Mean: {np.mean(values):.6e}")
+ print(f" Median: {np.median(values):.6e}")
+ print(f" Std Dev: {np.std(values):.6e}")
+ print(f" 25th Percentile: {np.percentile(values, 25):.6e}")
+ print(f" 75th Percentile: {np.percentile(values, 75):.6e}")
+ print(f" 95th Percentile: {np.percentile(values, 95):.6e}")
+ print(f" 99th Percentile: {np.percentile(values, 99):.6e}")
+
+ # Check for outliers (values beyond 3 standard deviations)
+ mean_val = np.mean(values)
+ std_val = np.std(values)
+ outliers = values[
+ (values < mean_val - 3 * std_val) | (values > mean_val + 3 * std_val)
+ ]
+ print(f" Outliers (ยฑ3ฯ): {len(outliers)} ({len(outliers)/len(values)*100:.1f}%)")
+
+ # Unique values count
+ unique_values = len(np.unique(values))
+ print(f" Unique Values: {unique_values}")
+ print()
+
+
+def get_material_triplets(segments):
+ """Extract material triplets from segments"""
+ triplets = []
+ for segment_name, segment_data in segments.items():
+ density = segment_data.get("density")
+ youngs_modulus = segment_data.get("youngs_modulus")
+ poissons_ratio = segment_data.get("poissons_ratio")
+ if all(x is not None for x in [density, youngs_modulus, poissons_ratio]):
+ triplets.append((density, youngs_modulus, poissons_ratio))
+ return triplets
+
+
+def get_material_types(segments):
+ """Extract material types from segments"""
+ return [
+ segment_data.get("material_type", "unknown")
+ for segment_data in segments.values()
+ ]
+
+
+def count_points_in_material_npz(file_path):
+ """Count total points in a material NPZ file"""
+ try:
+ data = np.load(file_path)
+ if len(data.files) > 0:
+ first_key = data.files[0]
+ return data[first_key].shape[0]
+ except Exception:
+ pass
+ return 0
+
+
+def print_stats():
+ # Load all datasets metadata (stored in simready directory)
+ metadata_df = pd.read_csv("datasets/simready/metadata.csv")
+
+ # Load material annotations
+ material_annotations = load_material_annotations()
+
+ # Create lookup for material annotations by file path
+ material_lookup = {}
+ for entry in material_annotations:
+ file_path = entry.get("file_path", "")
+ if file_path:
+ material_lookup[file_path] = entry
+
+ # Filter for objects that are rendered, have features, and are voxelized
+ filtered_df = metadata_df[
+ (metadata_df["rendered"] == True)
+ & (metadata_df["feature_dinov2_vitl14_reg"] == True)
+ & (metadata_df["voxelized"] == True)
+ ].copy()
+
+ print("=== DATASET STATISTICS ===")
+ print()
+
+ # Basic object counts
+ total_objects = len(filtered_df)
+ print(f"Total Objects: {total_objects}")
+ print()
+
+ # Objects per dataset
+ print("Objects per Dataset:")
+ dataset_counts = filtered_df["dataset"].value_counts()
+ for dataset, count in dataset_counts.items():
+ pct = (count / total_objects * 100) if total_objects > 0 else 0
+ print(f" {dataset}: {count} ({pct:.1f}%)")
+ print()
+
+ # Train/Val/Test splits - Total
+ print("Train/Val/Test Split - Total:")
+ split_counts = filtered_df["split"].value_counts()
+ for split in ["train", "val", "test"]:
+ count = split_counts.get(split, 0)
+ pct = (count / total_objects * 100) if total_objects > 0 else 0
+ print(f" {split}: {count} ({pct:.1f}%)")
+ print()
+
+ # Train/Val/Test splits per dataset
+ print("Train/Val/Test Split per Dataset:")
+ for dataset in dataset_counts.index:
+ dataset_df = filtered_df[filtered_df["dataset"] == dataset]
+ dataset_split_counts = dataset_df["split"].value_counts()
+ print(f" {dataset}:")
+ for split in ["train", "val", "test"]:
+ count = dataset_split_counts.get(split, 0)
+ pct = (count / len(dataset_df) * 100) if len(dataset_df) > 0 else 0
+ print(f" {split}: {count} ({pct:.1f}%)")
+ print()
+
+ # Segment statistics
+ total_segments = 0
+ segments_per_dataset = defaultdict(int)
+ material_triplets = []
+ triplets_per_dataset = defaultdict(list)
+ material_types_all = []
+ material_types_per_dataset = defaultdict(list)
+
+ # Material properties for statistical analysis
+ all_densities = []
+ all_youngs_moduli = []
+ all_poissons_ratios = []
+
+ objects_with_materials = 0
+
+ for _, row in filtered_df.iterrows():
+ file_path = row["local_path"]
+ dataset = row["dataset"]
+
+ # Get material annotation for this object
+ material_entry = material_lookup.get(file_path)
+ if material_entry:
+ objects_with_materials += 1
+ segments = material_entry.get("segments", {})
+ num_segments = len(segments)
+ total_segments += num_segments
+ segments_per_dataset[dataset] += num_segments
+
+ # Extract material triplets
+ triplets = get_material_triplets(segments)
+ material_triplets.extend(triplets)
+ triplets_per_dataset[dataset].extend(triplets)
+
+ # Extract material types
+ mat_types = get_material_types(segments)
+ material_types_all.extend(mat_types)
+ material_types_per_dataset[dataset].extend(mat_types)
+
+ # Extract material properties for statistical analysis
+ densities, youngs_moduli, poissons_ratios = get_material_properties_stats(
+ segments
+ )
+ all_densities.extend(densities)
+ all_youngs_moduli.extend(youngs_moduli)
+ all_poissons_ratios.extend(poissons_ratios)
+
+ print(f"Total Segments: {total_segments}")
+ segments_per_object = (
+ total_segments / objects_with_materials if objects_with_materials > 0 else 0
+ )
+ print(f"Average Segments per Object: {segments_per_object:.2f}")
+ print()
+
+ print("Segments per Dataset:")
+ for dataset, count in segments_per_dataset.items():
+ pct = (count / total_segments * 100) if total_segments > 0 else 0
+ print(f" {dataset}: {count} ({pct:.1f}%)")
+ print()
+
+ # Points in material NPZ files
+ total_points = 0
+ points_per_dataset = defaultdict(int)
+ points_per_object = []
+ points_per_object_per_dataset = defaultdict(list)
+ material_npz_dir = "datasets/simready/voxels"
+
+ for _, row in filtered_df.iterrows():
+ sha256 = row["sha256"]
+ dataset = row["dataset"]
+ material_file = f"{sha256}_with_materials.npz"
+ material_path = os.path.join(material_npz_dir, material_file)
+
+ if os.path.exists(material_path):
+ points = count_points_in_material_npz(material_path)
+ total_points += points
+ points_per_dataset[dataset] += points
+ points_per_object.append(points)
+ points_per_object_per_dataset[dataset].append(points)
+
+ print(f"Total Voxels in Material NPZ: {total_points}")
+ if points_per_object:
+ avg_points = np.mean(points_per_object)
+ std_points = np.std(points_per_object)
+ print(f"Average Voxels per Object: {avg_points:.0f} (ยฑ{std_points:.0f})")
+ print(f"Min Voxels per Object: {min(points_per_object)}")
+ print(f"Max Voxels per Object: {max(points_per_object)}")
+ print()
+
+ print("Total Voxels per Dataset:")
+ for dataset, points in points_per_dataset.items():
+ pct = (points / total_points * 100) if total_points > 0 else 0
+ print(f" {dataset}: {points} ({pct:.1f}%)")
+ print()
+
+ print("Average Voxels per Object per Dataset:")
+ for dataset in dataset_counts.index:
+ if (
+ dataset in points_per_object_per_dataset
+ and points_per_object_per_dataset[dataset]
+ ):
+ avg_voxels = np.mean(points_per_object_per_dataset[dataset])
+ print(f" {dataset}: {avg_voxels:.0f}")
+ else:
+ print(f" {dataset}: 0")
+ print()
+
+ # Unique material triplets
+ unique_triplets = set(material_triplets)
+ print(f"Total Unique Material Triplets: {len(unique_triplets)}")
+ print()
+
+ print("Unique Material Triplets per Dataset:")
+ for dataset, triplets in triplets_per_dataset.items():
+ unique_dataset_triplets = set(triplets)
+ print(f" {dataset}: {len(unique_dataset_triplets)}")
+ print()
+
+ # Material Properties Statistical Analysis
+ print("Material Properties Statistical Analysis:")
+ print_property_stats(all_densities, "Density", "kg/mยณ")
+ print_property_stats(all_youngs_moduli, "Young's Modulus", "Pa")
+ print_property_stats(all_poissons_ratios, "Poisson's Ratio", "dimensionless")
+
+ # Material type statistics
+ print("Material Type Counts (Top 10):")
+ material_type_counts = Counter(material_types_all)
+ for material_type, count in material_type_counts.most_common(10):
+ pct = (count / len(material_types_all) * 100) if material_types_all else 0
+ print(f" {material_type}: {count} ({pct:.1f}%)")
+
+ print(f"\nTotal Unique Material Types: {len(material_type_counts)}")
+ print()
+
+ # Object class statistics
+ print("Object Class Counts (All):")
+ class_counts = filtered_df["class"].value_counts()
+ for obj_class, count in class_counts.items():
+ pct = (count / total_objects * 100) if total_objects > 0 else 0
+ print(f" {obj_class}: {count} ({pct:.1f}%)")
+
+ print(f"\nTotal Unique Object Classes: {len(class_counts)}")
+ print()
+
+ # Dataset-specific material type analysis
+ print("Material Types per Dataset (Top 5 each):")
+ for dataset in dataset_counts.index:
+ dataset_materials = material_types_per_dataset[dataset]
+ if dataset_materials:
+ dataset_material_counts = Counter(dataset_materials)
+ print(f" {dataset}:")
+ for material_type, count in dataset_material_counts.most_common(5):
+ pct = (count / len(dataset_materials) * 100) if dataset_materials else 0
+ print(f" {material_type}: {count} ({pct:.1f}%)")
+ print()
+
+ # Generate LaTeX table
+ print("LaTeX Table:")
+ print("\\begin{tabular}{lrrrrrrrr}")
+ print("\\toprule")
+ print("\\rowcolor{blue!15}")
+ print(
+ "Dataset & Total Objects & Segments (\\%) & Voxels (\\%) & Avg. Segments/Object & Avg. Voxels/Object \\\\"
+ )
+ print("\\midrule")
+
+ # Calculate statistics per dataset for the table
+ table_data = []
+ total_objects_all = 0
+ total_segments_all = 0
+ total_voxels_all = 0
+ all_segments_per_obj = []
+ all_voxels_per_obj = []
+
+ for dataset in sorted(dataset_counts.index):
+ dataset_df = filtered_df[filtered_df["dataset"] == dataset]
+ dataset_objects = len(dataset_df)
+ dataset_segments = segments_per_dataset[dataset]
+ dataset_voxels = points_per_dataset[dataset]
+
+ # Calculate segments per object for this dataset
+ dataset_segments_per_obj = []
+ for _, row in dataset_df.iterrows():
+ file_path = row["local_path"]
+ material_entry = material_lookup.get(file_path)
+ if material_entry:
+ segments = material_entry.get("segments", {})
+ dataset_segments_per_obj.append(len(segments))
+
+ # Calculate voxels per object for this dataset
+ dataset_voxels_per_obj = points_per_object_per_dataset[dataset]
+
+ # Store for overall totals
+ total_objects_all += dataset_objects
+ total_segments_all += dataset_segments
+ total_voxels_all += dataset_voxels
+ all_segments_per_obj.extend(dataset_segments_per_obj)
+ all_voxels_per_obj.extend(dataset_voxels_per_obj)
+
+ # Calculate averages and std devs
+ avg_segments = (
+ np.mean(dataset_segments_per_obj) if dataset_segments_per_obj else 0
+ )
+ std_segments = (
+ np.std(dataset_segments_per_obj) if dataset_segments_per_obj else 0
+ )
+ avg_voxels = np.mean(dataset_voxels_per_obj) if dataset_voxels_per_obj else 0
+ std_voxels = np.std(dataset_voxels_per_obj) if dataset_voxels_per_obj else 0
+
+ table_data.append(
+ {
+ "dataset": dataset,
+ "objects": dataset_objects,
+ "segments": dataset_segments,
+ "voxels": dataset_voxels,
+ "avg_segments": avg_segments,
+ "std_segments": std_segments,
+ "avg_voxels": avg_voxels,
+ "std_voxels": std_voxels,
+ }
+ )
+
+ # Print each dataset row
+ for data in table_data:
+ segments_pct = (
+ (data["segments"] / total_segments_all * 100)
+ if total_segments_all > 0
+ else 0
+ )
+ voxels_pct = (
+ (data["voxels"] / total_voxels_all * 100) if total_voxels_all > 0 else 0
+ )
+
+ print(
+ f"{data['dataset']} & {data['objects']} & {data['segments']} ({segments_pct:.1f}) & {data['voxels']:,} ({voxels_pct:.1f}) & {data['avg_segments']:.2f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${data['std_segments']:.2f})}}}}}} & {data['avg_voxels']:,.0f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${data['std_voxels']:,.0f})}}}}}} \\\\"
+ )
+
+ print("\\midrule")
+
+ # Calculate overall totals
+ overall_avg_segments = np.mean(all_segments_per_obj) if all_segments_per_obj else 0
+ overall_std_segments = np.std(all_segments_per_obj) if all_segments_per_obj else 0
+ overall_avg_voxels = np.mean(all_voxels_per_obj) if all_voxels_per_obj else 0
+ overall_std_voxels = np.std(all_voxels_per_obj) if all_voxels_per_obj else 0
+
+ print(
+ f"\\textbf{{Total}} & {total_objects_all} & {total_segments_all} (100.0) & {total_voxels_all:,} (100.0) & {overall_avg_segments:.2f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${overall_std_segments:.2f})}}}}}} & {overall_avg_voxels:,.0f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${overall_std_voxels:,.0f})}}}}}} \\\\"
+ )
+
+ print("\\bottomrule")
+ print("\\end{tabular}")
+ print()
+
+ # Generate LaTeX table by splits
+ print("LaTeX Table by Splits:")
+ print("\\begin{tabular}{lrrrrrrrr}")
+ print("\\toprule")
+ print("\\rowcolor{blue!15}")
+ print(
+ "Dataset & Total Objects & Segments (\\%) & Voxels (\\%) & Avg. Segments/Object & Avg. Voxels/Object \\\\"
+ )
+ print("\\midrule")
+
+ # Calculate statistics per split for the table
+ split_table_data = []
+ split_names = ["train", "val", "test"] # Use 'val' as it appears in the data
+
+ for split in split_names:
+ split_df = filtered_df[filtered_df["split"] == split]
+ split_objects = len(split_df)
+
+ # Calculate segments for this split
+ split_segments = 0
+ split_segments_per_obj = []
+ for _, row in split_df.iterrows():
+ file_path = row["local_path"]
+ material_entry = material_lookup.get(file_path)
+ if material_entry:
+ segments = material_entry.get("segments", {})
+ num_segments = len(segments)
+ split_segments += num_segments
+ split_segments_per_obj.append(num_segments)
+
+ # Calculate voxels for this split
+ split_voxels = 0
+ split_voxels_per_obj = []
+ for _, row in split_df.iterrows():
+ sha256 = row["sha256"]
+ material_file = f"{sha256}_with_materials.npz"
+ material_path = os.path.join(material_npz_dir, material_file)
+
+ if os.path.exists(material_path):
+ points = count_points_in_material_npz(material_path)
+ split_voxels += points
+ split_voxels_per_obj.append(points)
+
+ # Calculate averages and std devs
+ avg_segments = np.mean(split_segments_per_obj) if split_segments_per_obj else 0
+ std_segments = np.std(split_segments_per_obj) if split_segments_per_obj else 0
+ avg_voxels = np.mean(split_voxels_per_obj) if split_voxels_per_obj else 0
+ std_voxels = np.std(split_voxels_per_obj) if split_voxels_per_obj else 0
+
+ # Convert 'val' to 'validation' for display
+ display_split = "validation" if split == "val" else split
+
+ split_table_data.append(
+ {
+ "split": display_split,
+ "objects": split_objects,
+ "segments": split_segments,
+ "voxels": split_voxels,
+ "avg_segments": avg_segments,
+ "std_segments": std_segments,
+ "avg_voxels": avg_voxels,
+ "std_voxels": std_voxels,
+ }
+ )
+
+ # Print each split row
+ for data in split_table_data:
+ segments_pct = (
+ (data["segments"] / total_segments_all * 100)
+ if total_segments_all > 0
+ else 0
+ )
+ voxels_pct = (
+ (data["voxels"] / total_voxels_all * 100) if total_voxels_all > 0 else 0
+ )
+
+ print(
+ f"{data['split']} & {data['objects']} & {data['segments']} ({segments_pct:.1f}) & {data['voxels']:,} ({voxels_pct:.1f}) & {data['avg_segments']:.2f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${data['std_segments']:.2f})}}}}}} & {data['avg_voxels']:,.0f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${data['std_voxels']:,.0f})}}}}}} \\\\"
+ )
+
+ print("\\midrule")
+
+ # Total row (same as before)
+ print(
+ f"\\textbf{{Total}} & {total_objects_all} & {total_segments_all} (100.0) & {total_voxels_all:,} (100.0) & {overall_avg_segments:.2f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${overall_std_segments:.2f})}}}}}} & {overall_avg_voxels:,.0f} {{\\textbf{{\\scriptsize\\textcolor{{gray}}{{($\\pm${overall_std_voxels:,.0f})}}}}}} \\\\"
+ )
+
+ print("\\bottomrule")
+ print("\\end{tabular}")
+ print()
+
+
+if __name__ == "__main__":
+ print_stats()
diff --git a/deps/vomp/scripts/evaluate_geometry_encoder.py b/deps/vomp/scripts/evaluate_geometry_encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..23193ad392bacf81145bee24c0983fef00550abc
--- /dev/null
+++ b/deps/vomp/scripts/evaluate_geometry_encoder.py
@@ -0,0 +1,1071 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import json
+import glob
+import argparse
+from easydict import EasyDict as edict
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import numpy as np
+from safetensors.torch import load_file
+from torch.utils.data import DataLoader
+
+from vomp.datasets.sparse_voxel_materials import SparseVoxelMaterials
+from vomp.models.geometry_encoder import (
+ ElasticGeometryEncoder,
+ ElasticSLatVoxelDecoder,
+)
+from vomp.models.material_vae.beta_tc import TripletVAE
+from vomp.utils.data_utils import recursive_to_device
+
+
+def l1_loss(pred, target):
+ return F.l1_loss(pred, target)
+
+
+def l2_loss(pred, target):
+ return F.mse_loss(pred, target)
+
+
+def find_ckpt(cfg):
+ cfg["load_ckpt"] = None
+ if cfg.load_dir != "":
+ if cfg.ckpt == "latest":
+
+ files = glob.glob(os.path.join(cfg.load_dir, "ckpts", "misc_step*.pt"))
+ if len(files) != 0:
+ cfg.load_ckpt = max(
+ [
+ int(os.path.basename(f).split("step")[-1].split(".")[0])
+ for f in files
+ ]
+ )
+ elif cfg.ckpt == "none":
+ cfg.load_ckpt = None
+ else:
+ cfg.load_ckpt = int(cfg.ckpt)
+ return cfg
+
+
+def load_model(cfg):
+
+ model_dict = {}
+
+ model_dict["geometry_encoder"] = ElasticGeometryEncoder(
+ **cfg.models.geometry_encoder.args
+ ).cuda()
+
+ if "decoder" in cfg.models:
+ model_dict["decoder"] = ElasticSLatVoxelDecoder(
+ **cfg.models.decoder.args
+ ).cuda()
+
+ matvae = TripletVAE(**cfg.models.matvae.args).cuda()
+ matvae_loaded = False
+ if cfg.get("matvae_checkpoint") is not None:
+ print(f"Loading matvae checkpoint from: {cfg.matvae_checkpoint}")
+ checkpoint = load_file(cfg.matvae_checkpoint)
+ matvae.load_state_dict(checkpoint, strict=True)
+ print("Successfully loaded matvae checkpoint")
+ matvae_loaded = True
+
+ if not matvae_loaded:
+ raise RuntimeError(
+ "MatVAE checkpoint was not loaded. Please ensure matvae_checkpoint is specified in the config."
+ )
+
+ matvae.eval()
+ for param in matvae.parameters():
+ param.requires_grad = False
+
+ geometry_encoder_loaded = False
+ if cfg.load_ckpt is not None:
+
+ geometry_encoder_path = os.path.join(
+ cfg.load_dir, "ckpts", f"geometry_encoder_step{cfg.load_ckpt:07d}.pt"
+ )
+ if os.path.exists(geometry_encoder_path):
+ print(f"Loading geometry encoder from: {geometry_encoder_path}")
+ checkpoint = torch.load(
+ geometry_encoder_path, map_location="cpu", weights_only=False
+ )
+ model_dict["geometry_encoder"].load_state_dict(checkpoint, strict=True)
+ print("Successfully loaded geometry encoder")
+ geometry_encoder_loaded = True
+ else:
+ raise RuntimeError(
+ f"Geometry encoder checkpoint not found at {geometry_encoder_path}"
+ )
+
+ if "decoder" in model_dict:
+ decoder_path = os.path.join(
+ cfg.load_dir, "ckpts", f"decoder_step{cfg.load_ckpt:07d}.pt"
+ )
+ if os.path.exists(decoder_path):
+ print(f"Loading decoder from: {decoder_path}")
+ checkpoint = torch.load(
+ decoder_path, map_location="cpu", weights_only=False
+ )
+ model_dict["decoder"].load_state_dict(checkpoint, strict=True)
+ print("Successfully loaded decoder")
+ else:
+ print(f"Warning: Decoder checkpoint not found at {decoder_path}")
+
+ if not geometry_encoder_loaded:
+ raise RuntimeError(
+ "Geometry encoder checkpoint was not loaded. Please ensure a valid checkpoint exists in the checkpoint directory."
+ )
+
+ for model in model_dict.values():
+ model.eval()
+
+ return model_dict, matvae
+
+
+@torch.inference_mode()
+def evaluate_model(model_dict, matvae, val_dataset, cfg):
+
+ print(f"Evaluating model on {len(val_dataset)} samples")
+
+ val_dataloader = DataLoader(
+ val_dataset,
+ batch_size=min(cfg.get("batch_size", 4), 2),
+ shuffle=False,
+ num_workers=4,
+ pin_memory=True,
+ persistent_workers=True,
+ collate_fn=(
+ val_dataset.collate_fn if hasattr(val_dataset, "collate_fn") else None
+ ),
+ )
+
+ total_loss = 0.0
+ total_loss_youngs = 0.0
+ total_loss_poisson = 0.0
+ total_loss_density = 0.0
+
+ all_pred_materials_original = []
+ all_gt_materials_original = []
+ object_identifiers = [] # Store object IDs/hashes
+ processed_samples = 0
+ total_voxels = 0
+
+ # Per-object tracking - collect during processing, not after
+ per_object_errors = {
+ "youngs": [],
+ "poisson": [],
+ "density": [],
+ "log_density": [],
+ "overall": [],
+ }
+ per_object_displacement_errors = {"youngs": [], "poisson": [], "density": []}
+ per_object_voxel_data = {"pred_values": [], "gt_values": [], "voxel_counts": []}
+
+ # Pre-collect object identifiers from the dataset
+ # The dataset has an instances attribute with (root, instance) tuples where instance is the hash
+ dataset_instances = []
+ if hasattr(val_dataset, "instances"):
+ dataset_instances = [instance for root, instance in val_dataset.instances]
+ print(f"Found {len(dataset_instances)} instance hashes in dataset")
+ else:
+ print("Warning: Dataset does not have instances attribute")
+
+ current_sample_idx = 0 # Track which sample we're processing
+
+ loss_type = cfg.get("trainer", {}).get("args", {}).get("loss_type", "l1")
+ lambda_youngs_modulus = cfg.get("lambda_youngs_modulus", 1.0)
+ lambda_poissons_ratio = cfg.get("lambda_poissons_ratio", 1.0)
+ lambda_density = cfg.get("lambda_density", 1.0)
+ training_mode = cfg.get("training_mode", "encoder_only")
+
+ print(f"Using loss type: {loss_type}")
+ print(f"Using training mode: {training_mode}")
+
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+ for batch_idx, data in enumerate(val_dataloader):
+
+ data = recursive_to_device(data, device, non_blocking=True)
+ feats = data["feats"]
+ materials = data["materials"]
+
+ num_voxels = feats.feats.shape[0]
+ num_samples = feats.shape[0]
+
+ # Collect object identifiers from the pre-collected dataset instances
+ batch_ids = []
+ for i in range(num_samples):
+ if current_sample_idx < len(dataset_instances):
+ batch_ids.append(dataset_instances[current_sample_idx])
+ current_sample_idx += 1
+ else:
+ batch_ids.append(f"unknown_sample_{current_sample_idx}")
+ current_sample_idx += 1
+
+ if batch_idx == 0: # Print once to show what identifier we're using
+ print(f"Using dataset instance hashes as object identifiers")
+
+ object_identifiers.extend(batch_ids)
+
+ z, mean, logvar = model_dict["geometry_encoder"](
+ feats, sample_posterior=False, return_raw=True
+ )
+ gt_materials_normalized = materials.feats
+
+ if training_mode == "encoder_only":
+
+ latent_2d = z.feats
+
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = matvae.decode(
+ latent_2d
+ )
+
+ E_pred = E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ nu_pred = nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ rho_pred = rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+
+ elif training_mode == "encoder_decoder_matvae":
+
+ decoder_output = model_dict["decoder"](z)
+ latent_2d = decoder_output.feats
+
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = matvae.decode(
+ latent_2d
+ )
+
+ E_pred = E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ nu_pred = nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ rho_pred = rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+
+ elif training_mode == "encoder_decoder_direct":
+
+ decoder_output = model_dict["decoder"](z)
+ material_predictions = decoder_output.feats
+
+ E_pred = material_predictions[:, 0]
+ nu_pred = material_predictions[:, 1]
+ rho_pred = material_predictions[:, 2]
+
+ else:
+ raise ValueError(f"Unknown training mode: {training_mode}")
+
+ if loss_type == "l1":
+ l1_E = l1_loss(E_pred, gt_materials_normalized[:, 0])
+ l1_nu = l1_loss(nu_pred, gt_materials_normalized[:, 1])
+ l1_rho = l1_loss(rho_pred, gt_materials_normalized[:, 2])
+
+ loss = (
+ lambda_youngs_modulus * l1_E
+ + lambda_poissons_ratio * l1_nu
+ + lambda_density * l1_rho
+ )
+ loss_youngs = l1_E
+ loss_poisson = l1_nu
+ loss_density = l1_rho
+
+ elif loss_type == "l2":
+ l2_E = l2_loss(E_pred, gt_materials_normalized[:, 0])
+ l2_nu = l2_loss(nu_pred, gt_materials_normalized[:, 1])
+ l2_rho = l2_loss(rho_pred, gt_materials_normalized[:, 2])
+
+ loss = (
+ lambda_youngs_modulus * l2_E
+ + lambda_poissons_ratio * l2_nu
+ + lambda_density * l2_rho
+ )
+ loss_youngs = l2_E
+ loss_poisson = l2_nu
+ loss_density = l2_rho
+
+ total_loss += loss.item() * num_voxels
+ total_loss_youngs += loss_youngs.item() * num_voxels
+ total_loss_poisson += loss_poisson.item() * num_voxels
+ total_loss_density += loss_density.item() * num_voxels
+
+ pred_materials_normalized = torch.stack([E_pred, nu_pred, rho_pred], dim=-1)
+
+ pred_materials_original = (
+ val_dataset.material_transform.destandardize_and_inverse_transform_tensor(
+ pred_materials_normalized
+ )
+ )
+ gt_materials_original = (
+ val_dataset.material_transform.destandardize_and_inverse_transform_tensor(
+ gt_materials_normalized
+ )
+ )
+
+ all_pred_materials_original.append(pred_materials_original.cpu())
+ all_gt_materials_original.append(gt_materials_original.cpu())
+
+ # Compute per-object statistics correctly using SparseTensor structure
+ batch_coords = feats.coords # Shape: [N, 4] where first column is batch index
+
+ # Get material values for error computation
+ pred_E_batch = pred_materials_original[:, 0]
+ pred_nu_batch = pred_materials_original[:, 1]
+ pred_rho_batch = pred_materials_original[:, 2]
+
+ gt_E_batch = gt_materials_original[:, 0]
+ gt_nu_batch = gt_materials_original[:, 1]
+ gt_rho_batch = gt_materials_original[:, 2]
+
+ # Compute relative errors for this batch
+ log_pred_E_batch = torch.log10(torch.clamp_min(pred_E_batch, 1e-8))
+ log_gt_E_batch = torch.log10(torch.clamp_min(gt_E_batch, 1e-8))
+ rel_error_log_E_batch = torch.abs(
+ log_pred_E_batch - log_gt_E_batch
+ ) / torch.abs(log_gt_E_batch)
+
+ rel_error_nu_batch = torch.abs(pred_nu_batch - gt_nu_batch) / torch.abs(
+ gt_nu_batch
+ )
+ rel_error_rho_batch = torch.abs(pred_rho_batch - gt_rho_batch) / torch.abs(
+ gt_rho_batch
+ )
+
+ # Log-space density errors
+ log_pred_rho_batch = torch.log10(torch.clamp_min(pred_rho_batch, 1e-8))
+ log_gt_rho_batch = torch.log10(torch.clamp_min(gt_rho_batch, 1e-8))
+ rel_error_log_rho_batch = torch.abs(
+ log_pred_rho_batch - log_gt_rho_batch
+ ) / torch.abs(log_gt_rho_batch)
+
+ # Compute displacement errors for this batch
+ abs_error_log_E_batch = torch.abs(
+ torch.log10(torch.clamp_min(pred_E_batch, 1e-8))
+ - torch.log10(torch.clamp_min(gt_E_batch, 1e-8))
+ )
+ abs_error_nu_batch = torch.abs(pred_nu_batch - gt_nu_batch)
+ abs_error_rho_batch = torch.abs(pred_rho_batch - gt_rho_batch)
+
+ # Process each object in the batch separately
+ for obj_in_batch in range(num_samples):
+ # Find voxels belonging to this object (batch index == obj_in_batch)
+ obj_voxel_mask = batch_coords[:, 0] == obj_in_batch
+ num_obj_voxels = obj_voxel_mask.sum().item()
+
+ if num_obj_voxels == 0:
+ continue
+
+ # Extract relative error data for this specific object
+ obj_rel_error_log_E = rel_error_log_E_batch[obj_voxel_mask].cpu()
+ obj_rel_error_nu = rel_error_nu_batch[obj_voxel_mask].cpu()
+ obj_rel_error_rho = rel_error_rho_batch[obj_voxel_mask].cpu()
+ obj_rel_error_log_rho = rel_error_log_rho_batch[obj_voxel_mask].cpu()
+
+ # Extract displacement error data for this specific object
+ obj_abs_error_log_E = abs_error_log_E_batch[obj_voxel_mask].cpu()
+ obj_abs_error_nu = abs_error_nu_batch[obj_voxel_mask].cpu()
+ obj_abs_error_rho = abs_error_rho_batch[obj_voxel_mask].cpu()
+
+ # Compute per-object relative errors
+ per_object_errors["youngs"].append(obj_rel_error_log_E.mean().item())
+ per_object_errors["poisson"].append(obj_rel_error_nu.mean().item())
+ per_object_errors["density"].append(obj_rel_error_rho.mean().item())
+ per_object_errors["log_density"].append(obj_rel_error_log_rho.mean().item())
+
+ # Compute per-object displacement errors
+ per_object_displacement_errors["youngs"].append(
+ obj_abs_error_log_E.mean().item()
+ )
+ per_object_displacement_errors["poisson"].append(
+ obj_abs_error_nu.mean().item()
+ )
+ per_object_displacement_errors["density"].append(
+ obj_abs_error_rho.mean().item()
+ )
+
+ # Compute overall error for this object (using log-space for Young's modulus and density)
+ obj_all_errors = torch.cat(
+ [obj_rel_error_log_E, obj_rel_error_nu, obj_rel_error_log_rho]
+ )
+ per_object_errors["overall"].append(obj_all_errors.mean().item())
+
+ # Store voxel-level data for this object
+ obj_pred_values = {
+ "youngs": pred_E_batch[obj_voxel_mask].cpu(),
+ "poisson": pred_nu_batch[obj_voxel_mask].cpu(),
+ "density": pred_rho_batch[obj_voxel_mask].cpu(),
+ }
+ obj_gt_values = {
+ "youngs": gt_E_batch[obj_voxel_mask].cpu(),
+ "poisson": gt_nu_batch[obj_voxel_mask].cpu(),
+ "density": gt_rho_batch[obj_voxel_mask].cpu(),
+ }
+
+ per_object_voxel_data["pred_values"].append(obj_pred_values)
+ per_object_voxel_data["gt_values"].append(obj_gt_values)
+ per_object_voxel_data["voxel_counts"].append(num_obj_voxels)
+
+ processed_samples += num_samples
+ total_voxels += num_voxels
+
+ if batch_idx % 10 == 0:
+ print(f"Processed {processed_samples} samples ({total_voxels} voxels)...")
+
+ val_metrics = {
+ "val_loss": total_loss / total_voxels if total_voxels > 0 else 0.0,
+ "val_loss_youngs": (
+ total_loss_youngs / total_voxels if total_voxels > 0 else 0.0
+ ),
+ "val_loss_poisson": (
+ total_loss_poisson / total_voxels if total_voxels > 0 else 0.0
+ ),
+ "val_loss_density": (
+ total_loss_density / total_voxels if total_voxels > 0 else 0.0
+ ),
+ "val_samples": processed_samples,
+ }
+
+ if all_pred_materials_original and all_gt_materials_original:
+ all_pred_materials = torch.cat(all_pred_materials_original, dim=0)
+ all_gt_materials = torch.cat(all_gt_materials_original, dim=0)
+
+ pred_E = all_pred_materials[:, 0]
+ pred_nu = all_pred_materials[:, 1]
+ pred_rho = all_pred_materials[:, 2]
+
+ gt_E = all_gt_materials[:, 0]
+ gt_nu = all_gt_materials[:, 1]
+ gt_rho = all_gt_materials[:, 2]
+
+ log_pred_E = torch.log10(torch.clamp_min(pred_E, 1e-8))
+ log_gt_E = torch.log10(torch.clamp_min(gt_E, 1e-8))
+ rel_error_log_E = torch.abs(log_pred_E - log_gt_E) / torch.abs(log_gt_E)
+ val_metrics["val_rel_error_log_youngs"] = rel_error_log_E.mean().item()
+ val_metrics["val_rel_error_log_youngs_std"] = rel_error_log_E.std().item()
+
+ rel_error_nu = torch.abs(pred_nu - gt_nu) / torch.abs(gt_nu)
+ val_metrics["val_rel_error_poisson"] = rel_error_nu.mean().item()
+ val_metrics["val_rel_error_poisson_std"] = rel_error_nu.std().item()
+
+ rel_error_rho = torch.abs(pred_rho - gt_rho) / torch.abs(gt_rho)
+ val_metrics["val_rel_error_density"] = rel_error_rho.mean().item()
+ val_metrics["val_rel_error_density_std"] = rel_error_rho.std().item()
+
+ # Log-space density errors
+ log_pred_rho = torch.log10(torch.clamp_min(pred_rho, 1e-8))
+ log_gt_rho = torch.log10(torch.clamp_min(gt_rho, 1e-8))
+ rel_error_log_rho = torch.abs(log_pred_rho - log_gt_rho) / torch.abs(log_gt_rho)
+ val_metrics["val_rel_error_log_density"] = rel_error_log_rho.mean().item()
+ val_metrics["val_rel_error_log_density_std"] = rel_error_log_rho.std().item()
+
+ # Displacement errors (absolute errors)
+ abs_error_E = torch.abs(pred_E - gt_E)
+ abs_error_nu = torch.abs(pred_nu - gt_nu)
+ abs_error_rho = torch.abs(pred_rho - gt_rho)
+
+ # Log displacement errors for Young's modulus
+ abs_error_log_E = torch.abs(log_pred_E - log_gt_E)
+
+ val_metrics["val_abs_error_youngs"] = abs_error_E.mean().item()
+ val_metrics["val_abs_error_youngs_std"] = abs_error_E.std().item()
+ val_metrics["val_abs_error_log_youngs"] = abs_error_log_E.mean().item()
+ val_metrics["val_abs_error_log_youngs_std"] = abs_error_log_E.std().item()
+
+ val_metrics["val_abs_error_poisson"] = abs_error_nu.mean().item()
+ val_metrics["val_abs_error_poisson_std"] = abs_error_nu.std().item()
+
+ val_metrics["val_abs_error_density"] = abs_error_rho.mean().item()
+ val_metrics["val_abs_error_density_std"] = abs_error_rho.std().item()
+
+ all_rel_errors = torch.cat([rel_error_log_E, rel_error_nu, rel_error_log_rho])
+ val_metrics["val_rel_error_overall"] = all_rel_errors.mean().item()
+ val_metrics["val_rel_error_overall_std"] = all_rel_errors.std().item()
+
+ per_object_errors_tensor = {
+ "youngs": torch.tensor(per_object_errors["youngs"]),
+ "poisson": torch.tensor(per_object_errors["poisson"]),
+ "density": torch.tensor(per_object_errors["density"]),
+ "log_density": torch.tensor(per_object_errors["log_density"]),
+ "overall": torch.tensor(per_object_errors["overall"]),
+ }
+
+ val_metrics["per_object_rel_error_log_youngs_mean"] = (
+ per_object_errors_tensor["youngs"].mean().item()
+ )
+ val_metrics["per_object_rel_error_log_youngs_std"] = (
+ per_object_errors_tensor["youngs"].std().item()
+ )
+ val_metrics["per_object_rel_error_log_youngs_min"] = (
+ per_object_errors_tensor["youngs"].min().item()
+ )
+ val_metrics["per_object_rel_error_log_youngs_max"] = (
+ per_object_errors_tensor["youngs"].max().item()
+ )
+
+ val_metrics["per_object_rel_error_poisson_mean"] = (
+ per_object_errors_tensor["poisson"].mean().item()
+ )
+ val_metrics["per_object_rel_error_poisson_std"] = (
+ per_object_errors_tensor["poisson"].std().item()
+ )
+ val_metrics["per_object_rel_error_poisson_min"] = (
+ per_object_errors_tensor["poisson"].min().item()
+ )
+ val_metrics["per_object_rel_error_poisson_max"] = (
+ per_object_errors_tensor["poisson"].max().item()
+ )
+
+ val_metrics["per_object_rel_error_density_mean"] = (
+ per_object_errors_tensor["density"].mean().item()
+ )
+ val_metrics["per_object_rel_error_density_std"] = (
+ per_object_errors_tensor["density"].std().item()
+ )
+ val_metrics["per_object_rel_error_density_min"] = (
+ per_object_errors_tensor["density"].min().item()
+ )
+ val_metrics["per_object_rel_error_density_max"] = (
+ per_object_errors_tensor["density"].max().item()
+ )
+
+ val_metrics["per_object_rel_error_log_density_mean"] = (
+ per_object_errors_tensor["log_density"].mean().item()
+ )
+ val_metrics["per_object_rel_error_log_density_std"] = (
+ per_object_errors_tensor["log_density"].std().item()
+ )
+ val_metrics["per_object_rel_error_log_density_min"] = (
+ per_object_errors_tensor["log_density"].min().item()
+ )
+ val_metrics["per_object_rel_error_log_density_max"] = (
+ per_object_errors_tensor["log_density"].max().item()
+ )
+
+ val_metrics["per_object_rel_error_overall_mean"] = (
+ per_object_errors_tensor["overall"].mean().item()
+ )
+ val_metrics["per_object_rel_error_overall_std"] = (
+ per_object_errors_tensor["overall"].std().item()
+ )
+ val_metrics["per_object_rel_error_overall_min"] = (
+ per_object_errors_tensor["overall"].min().item()
+ )
+ val_metrics["per_object_rel_error_overall_max"] = (
+ per_object_errors_tensor["overall"].max().item()
+ )
+
+ # Add per-object displacement error statistics
+ per_object_displacement_errors_tensor = {
+ "youngs": torch.tensor(per_object_displacement_errors["youngs"]),
+ "poisson": torch.tensor(per_object_displacement_errors["poisson"]),
+ "density": torch.tensor(per_object_displacement_errors["density"]),
+ }
+
+ val_metrics["per_object_abs_error_log_youngs_mean"] = (
+ per_object_displacement_errors_tensor["youngs"].mean().item()
+ )
+ val_metrics["per_object_abs_error_log_youngs_std"] = (
+ per_object_displacement_errors_tensor["youngs"].std().item()
+ )
+
+ val_metrics["per_object_abs_error_poisson_mean"] = (
+ per_object_displacement_errors_tensor["poisson"].mean().item()
+ )
+ val_metrics["per_object_abs_error_poisson_std"] = (
+ per_object_displacement_errors_tensor["poisson"].std().item()
+ )
+
+ val_metrics["per_object_abs_error_density_mean"] = (
+ per_object_displacement_errors_tensor["density"].mean().item()
+ )
+ val_metrics["per_object_abs_error_density_std"] = (
+ per_object_displacement_errors_tensor["density"].std().item()
+ )
+
+ val_metrics["per_object_errors"] = per_object_errors
+ val_metrics["per_object_displacement_errors"] = per_object_displacement_errors
+ val_metrics["object_identifiers"] = object_identifiers
+ val_metrics["per_object_voxel_data"] = per_object_voxel_data
+
+ return val_metrics
+
+
+def main():
+
+ parser = argparse.ArgumentParser(
+ description="Evaluate trained geometry encoder model"
+ )
+ parser.add_argument(
+ "--config",
+ type=str,
+ required=True,
+ help="Path to the config file used during training",
+ )
+ parser.add_argument(
+ "--checkpoint_dir",
+ type=str,
+ required=True,
+ help="Directory containing model checkpoints",
+ )
+ parser.add_argument(
+ "--ckpt",
+ type=str,
+ default="latest",
+ help="Checkpoint to load (latest, none, or step number)",
+ )
+ parser.add_argument(
+ "--data_dir", type=str, default="./data/", help="Data directory"
+ )
+ parser.add_argument(
+ "--results",
+ action="store_true",
+ help="Print only voxel-level results in LaTeX table format",
+ )
+
+ args = parser.parse_args()
+
+ with open(args.config, "r") as f:
+ config = json.load(f)
+
+ cfg = edict()
+ cfg.update(config)
+ cfg.load_dir = args.checkpoint_dir
+ cfg.ckpt = args.ckpt
+ cfg.data_dir = args.data_dir
+
+ print("Evaluation Config:")
+ print("=" * 50)
+ print(f"Config file: {args.config}")
+ print(f"Checkpoint dir: {args.checkpoint_dir}")
+ print(f"Checkpoint: {args.ckpt}")
+ print(f"Data dir: {args.data_dir}")
+
+ cfg = find_ckpt(cfg)
+ if cfg.load_ckpt is not None:
+ print(f"Will load checkpoint step: {cfg.load_ckpt}")
+ else:
+ print("No checkpoint found to load")
+
+ # Determine normalization parameters file path
+ normalization_params_file = None
+ if cfg.get("matvae_checkpoint") is not None:
+ # Extract the directory containing the matvae checkpoint
+ matvae_checkpoint_path = cfg.matvae_checkpoint
+
+ # Navigate to the project directory (up from checkpoints/checkpoint_X/)
+ if "checkpoints" in matvae_checkpoint_path:
+ # For paths like: outputs/matvae/checkpoints/checkpoint_821/model.safetensors
+ # Need to go up 3 levels: model.safetensors -> checkpoint_X -> checkpoints -> project_dir
+ matvae_project_dir = os.path.dirname(
+ os.path.dirname(os.path.dirname(matvae_checkpoint_path))
+ )
+ normalization_params_file = os.path.join(
+ matvae_project_dir, "normalization_params.json"
+ )
+ else:
+ # For other checkpoint path formats, try to find the normalization params
+ checkpoint_dir = os.path.dirname(matvae_checkpoint_path)
+ normalization_params_file = os.path.join(
+ checkpoint_dir, "normalization_params.json"
+ )
+
+ if os.path.exists(normalization_params_file):
+ print(f"Found normalization parameters file: {normalization_params_file}")
+ else:
+ print(
+ f"ERROR: Could not find normalization parameters file at {normalization_params_file}"
+ )
+ print(
+ "This file is required for consistent normalization between matvae and geometry encoder training!"
+ )
+ print(
+ "Make sure the matvae was trained with the updated train_material_vae.py that saves normalization_params.json"
+ )
+ raise FileNotFoundError(
+ f"Required normalization parameters file not found: {normalization_params_file}"
+ )
+ else:
+ print("ERROR: No matvae_checkpoint specified in config!")
+ print(
+ "The matvae_checkpoint is required to locate the normalization parameters file."
+ )
+ raise ValueError(
+ "matvae_checkpoint must be specified in config to load normalization parameters"
+ )
+
+ val_dataset_args = cfg.dataset.args.copy()
+ val_dataset_args["split"] = "test"
+
+ if "normalization_type" in cfg.dataset:
+ val_dataset_args["normalization_type"] = cfg.dataset.normalization_type
+ print(f"Using material normalization type: {cfg.dataset.normalization_type}")
+
+ # Add normalization parameters file (now mandatory)
+ val_dataset_args["normalization_params_file"] = normalization_params_file
+
+ val_dataset = SparseVoxelMaterials(**val_dataset_args)
+ print(f"Loaded test dataset with {len(val_dataset)} samples")
+
+ model_dict, matvae = load_model(cfg)
+
+ print("\nStarting evaluation...")
+ print("=" * 50)
+
+ val_metrics = evaluate_model(model_dict, matvae, val_dataset, cfg)
+
+ # Check if --results flag was passed for LaTeX table format output
+ if args.results:
+ # Print only voxel-level results in LaTeX table format
+ alde_youngs = val_metrics["val_abs_error_log_youngs"]
+ alde_youngs_std = val_metrics["val_abs_error_log_youngs_std"]
+ alre_youngs = val_metrics["val_rel_error_log_youngs"]
+ alre_youngs_std = val_metrics["val_rel_error_log_youngs_std"]
+ ade_poisson = val_metrics["val_abs_error_poisson"]
+ ade_poisson_std = val_metrics["val_abs_error_poisson_std"]
+ are_poisson = val_metrics["val_rel_error_poisson"]
+ are_poisson_std = val_metrics["val_rel_error_poisson_std"]
+ ade_density = val_metrics["val_abs_error_density"]
+ ade_density_std = val_metrics["val_abs_error_density_std"]
+ are_density = val_metrics["val_rel_error_density"]
+ are_density_std = val_metrics["val_rel_error_density_std"]
+
+ print(
+ "%.4f {\\textbf{\\scriptsize\\textcolor{gray}{($\\pm$%.2f)}}} & "
+ % (alde_youngs, alde_youngs_std)
+ + "%.4f {\\textbf{\\scriptsize\\textcolor{gray}{($\\pm$%.2f)}}} & "
+ % (alre_youngs, alre_youngs_std)
+ + "%.4f {\\textbf{\\scriptsize\\textcolor{gray}{($\\pm$%.2f)}}} & "
+ % (ade_poisson, ade_poisson_std)
+ + "%.4f {\\textbf{\\scriptsize\\textcolor{gray}{($\\pm$%.2f)}}} & "
+ % (are_poisson, are_poisson_std)
+ + "%.4f {\\textbf{\\scriptsize\\textcolor{gray}{($\\pm$%.2f)}}} & "
+ % (ade_density, ade_density_std)
+ + "%.4f {\\textbf{\\scriptsize\\textcolor{gray}{($\\pm$%.2f)}}}"
+ % (are_density, are_density_std)
+ )
+ return
+
+ print("\n" + "=" * 80)
+ print("COMPREHENSIVE RELATIVE ERROR ANALYSIS")
+ print("=" * 80)
+
+ print(f"\nDataset Summary:")
+ print(f" Total samples processed: {val_metrics['val_samples']}")
+
+ print(f"\n1. VOXEL-LEVEL RELATIVE ERRORS (computed on all voxels):")
+ print("-" * 60)
+ print(f"Young's modulus (log space):")
+ print(
+ f" Mean: {val_metrics['val_rel_error_log_youngs']:.6f} ({val_metrics['val_rel_error_log_youngs']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['val_rel_error_log_youngs_std']:.6f} ({val_metrics['val_rel_error_log_youngs_std']*100:.2f}%)"
+ )
+
+ print(f"\nPoisson's ratio:")
+ print(
+ f" Mean: {val_metrics['val_rel_error_poisson']:.6f} ({val_metrics['val_rel_error_poisson']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['val_rel_error_poisson_std']:.6f} ({val_metrics['val_rel_error_poisson_std']*100:.2f}%)"
+ )
+
+ print(f"\nDensity:")
+ print(
+ f" Mean: {val_metrics['val_rel_error_density']:.6f} ({val_metrics['val_rel_error_density']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['val_rel_error_density_std']:.6f} ({val_metrics['val_rel_error_density_std']*100:.2f}%)"
+ )
+
+ print(f"\nDensity (log space):")
+ print(
+ f" Mean: {val_metrics['val_rel_error_log_density']:.6f} ({val_metrics['val_rel_error_log_density']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['val_rel_error_log_density_std']:.6f} ({val_metrics['val_rel_error_log_density_std']*100:.2f}%)"
+ )
+
+ print(f"\nOverall (all properties combined):")
+ print(
+ f" Mean: {val_metrics['val_rel_error_overall']:.6f} ({val_metrics['val_rel_error_overall']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['val_rel_error_overall_std']:.6f} ({val_metrics['val_rel_error_overall_std']*100:.2f}%)"
+ )
+
+ print(f"\n2. PER-OBJECT RELATIVE ERRORS (averaged per object):")
+ print("-" * 60)
+ print(f"Young's modulus (log space):")
+ print(
+ f" Mean: {val_metrics['per_object_rel_error_log_youngs_mean']:.6f} ({val_metrics['per_object_rel_error_log_youngs_mean']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['per_object_rel_error_log_youngs_std']:.6f} ({val_metrics['per_object_rel_error_log_youngs_std']*100:.2f}%)"
+ )
+ print(
+ f" Min: {val_metrics['per_object_rel_error_log_youngs_min']:.6f} ({val_metrics['per_object_rel_error_log_youngs_min']*100:.2f}%)"
+ )
+ print(
+ f" Max: {val_metrics['per_object_rel_error_log_youngs_max']:.6f} ({val_metrics['per_object_rel_error_log_youngs_max']*100:.2f}%)"
+ )
+
+ print(f"\nPoisson's ratio:")
+ print(
+ f" Mean: {val_metrics['per_object_rel_error_poisson_mean']:.6f} ({val_metrics['per_object_rel_error_poisson_mean']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['per_object_rel_error_poisson_std']:.6f} ({val_metrics['per_object_rel_error_poisson_std']*100:.2f}%)"
+ )
+ print(
+ f" Min: {val_metrics['per_object_rel_error_poisson_min']:.6f} ({val_metrics['per_object_rel_error_poisson_min']*100:.2f}%)"
+ )
+ print(
+ f" Max: {val_metrics['per_object_rel_error_poisson_max']:.6f} ({val_metrics['per_object_rel_error_poisson_max']*100:.2f}%)"
+ )
+
+ print(f"\nDensity:")
+ print(
+ f" Mean: {val_metrics['per_object_rel_error_density_mean']:.6f} ({val_metrics['per_object_rel_error_density_mean']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['per_object_rel_error_density_std']:.6f} ({val_metrics['per_object_rel_error_density_std']*100:.2f}%)"
+ )
+ print(
+ f" Min: {val_metrics['per_object_rel_error_density_min']:.6f} ({val_metrics['per_object_rel_error_density_min']*100:.2f}%)"
+ )
+ print(
+ f" Max: {val_metrics['per_object_rel_error_density_max']:.6f} ({val_metrics['per_object_rel_error_density_max']*100:.2f}%)"
+ )
+
+ print(f"\nDensity (log space):")
+ print(
+ f" Mean: {val_metrics['per_object_rel_error_log_density_mean']:.6f} ({val_metrics['per_object_rel_error_log_density_mean']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['per_object_rel_error_log_density_std']:.6f} ({val_metrics['per_object_rel_error_log_density_std']*100:.2f}%)"
+ )
+ print(
+ f" Min: {val_metrics['per_object_rel_error_log_density_min']:.6f} ({val_metrics['per_object_rel_error_log_density_min']*100:.2f}%)"
+ )
+ print(
+ f" Max: {val_metrics['per_object_rel_error_log_density_max']:.6f} ({val_metrics['per_object_rel_error_log_density_max']*100:.2f}%)"
+ )
+
+ print(f"\nOverall (all properties combined):")
+ print(
+ f" Mean: {val_metrics['per_object_rel_error_overall_mean']:.6f} ({val_metrics['per_object_rel_error_overall_mean']*100:.2f}%)"
+ )
+ print(
+ f" Std: {val_metrics['per_object_rel_error_overall_std']:.6f} ({val_metrics['per_object_rel_error_overall_std']*100:.2f}%)"
+ )
+ print(
+ f" Min: {val_metrics['per_object_rel_error_overall_min']:.6f} ({val_metrics['per_object_rel_error_overall_min']*100:.2f}%)"
+ )
+ print(
+ f" Max: {val_metrics['per_object_rel_error_overall_max']:.6f} ({val_metrics['per_object_rel_error_overall_max']*100:.2f}%)"
+ )
+
+ print(f"\n3. ERROR DISTRIBUTION ANALYSIS:")
+ print("-" * 60)
+ import numpy as np
+
+ per_obj_errors = val_metrics["per_object_errors"]
+ object_ids = val_metrics.get("object_identifiers", [])
+
+ for prop_name, prop_key in [
+ ("Young's modulus", "youngs"),
+ ("Poisson's ratio", "poisson"),
+ ("Density", "density"),
+ ("Density (log space)", "log_density"),
+ ]:
+ errors = np.array(per_obj_errors[prop_key])
+
+ p25, p50, p75, p90, p95, p99 = np.percentile(errors, [25, 50, 75, 90, 95, 99])
+
+ print(f"\n{prop_name} per-object error percentiles:")
+ print(f" 25th: {p25:.4f} ({p25*100:.2f}%)")
+ print(f" 50th: {p50:.4f} ({p50*100:.2f}%)")
+ print(f" 75th: {p75:.4f} ({p75*100:.2f}%)")
+ print(f" 90th: {p90:.4f} ({p90*100:.2f}%)")
+ print(f" 95th: {p95:.4f} ({p95*100:.2f}%)")
+ print(f" 99th: {p99:.4f} ({p99*100:.2f}%)")
+
+ worst_objects = np.argsort(errors)[-5:]
+ if object_ids:
+ worst_object_ids = [object_ids[i] for i in worst_objects]
+ worst_object_errors = [f"{errors[i]:.4f}" for i in worst_objects]
+ print(f" Top 5 worst objects:")
+ for obj_id, error in zip(worst_object_ids, worst_object_errors):
+ print(f" {obj_id}: {error} ({float(error)*100:.2f}%)")
+ else:
+ print(
+ f" Top 5 worst objects (indices): {worst_objects} with errors: {[f'{errors[i]:.4f}' for i in worst_objects]}"
+ )
+
+ # Show detailed voxel analysis for density (the most problematic property)
+ if prop_key == "density" and "per_object_voxel_data" in val_metrics:
+ voxel_data = val_metrics["per_object_voxel_data"]
+
+ for rank, obj_idx in enumerate(worst_objects):
+ obj_id = object_ids[obj_idx] if object_ids else f"object_{obj_idx}"
+ error = errors[obj_idx]
+
+ # Get the voxel data for this object
+ if obj_idx < len(voxel_data["pred_values"]):
+ pred_values = voxel_data["pred_values"][obj_idx]
+ gt_values = voxel_data["gt_values"][obj_idx]
+ num_voxels = voxel_data["voxel_counts"][obj_idx]
+
+ print(
+ f"\n Object {rank+1}: {obj_id} (Error: {error:.4f}, {error*100:.2f}%)"
+ )
+ print(f" Total voxels: {num_voxels}")
+ print(f" 5 random voxels (Predicted vs Ground Truth):")
+
+ # Get 5 random voxel indices
+ if num_voxels > 0:
+ random_indices = np.random.choice(
+ num_voxels, min(5, num_voxels), replace=False
+ )
+
+ for i, voxel_idx in enumerate(random_indices):
+ pred_density = pred_values["density"][voxel_idx].item()
+ gt_density = gt_values["density"][voxel_idx].item()
+ rel_error = abs(pred_density - gt_density) / abs(gt_density)
+
+ print(
+ f" Voxel {i+1}: Pred={pred_density:.2f}, GT={gt_density:.2f}, RelErr={rel_error:.4f} ({rel_error*100:.2f}%)"
+ )
+ else:
+ print(" No voxels found for this object")
+
+ print(f"\n4. INTERPRETATION:")
+ print("-" * 60)
+
+ density_std_ratio = (
+ val_metrics["per_object_rel_error_density_std"]
+ / val_metrics["per_object_rel_error_density_mean"]
+ )
+ log_density_std_ratio = (
+ val_metrics["per_object_rel_error_log_density_std"]
+ / val_metrics["per_object_rel_error_log_density_mean"]
+ )
+ youngs_std_ratio = (
+ val_metrics["per_object_rel_error_log_youngs_std"]
+ / val_metrics["per_object_rel_error_log_youngs_mean"]
+ )
+ poisson_std_ratio = (
+ val_metrics["per_object_rel_error_poisson_std"]
+ / val_metrics["per_object_rel_error_poisson_mean"]
+ )
+
+ print(f"Coefficient of Variation (std/mean) across objects:")
+ print(f" Young's modulus: {youngs_std_ratio:.3f}")
+ print(f" Poisson's ratio: {poisson_std_ratio:.3f}")
+ print(f" Density: {density_std_ratio:.3f}")
+ print(f" Density (log space): {log_density_std_ratio:.3f}")
+
+ print("\n" + "=" * 80)
+
+ # Summary tables with displacement and relative errors
+ print("\n5. SUMMARY TABLES:")
+ print("=" * 80)
+
+ # Table 1: Voxel-level averages
+ print("\nTable 1: VOXEL-LEVEL AVERAGES (computed on all voxels)")
+ print("-" * 100)
+ print(
+ f"{'Property':<20} {'Avg Displacement Error ยฑ Std':<35} {'Avg Relative Error ยฑ Std':<35}"
+ )
+ print("-" * 100)
+
+ # Young's modulus (log space)
+ log_disp_youngs = val_metrics["val_abs_error_log_youngs"]
+ log_disp_youngs_std = val_metrics["val_abs_error_log_youngs_std"]
+ log_rel_youngs = val_metrics["val_rel_error_log_youngs"]
+ log_rel_youngs_std = val_metrics["val_rel_error_log_youngs_std"]
+ print(
+ f"{'Youngs Modulus':<20} {f'{log_disp_youngs:.4f}ยฑ{log_disp_youngs_std:.2f} (log)':<35} {f'{log_rel_youngs:.4f}ยฑ{log_rel_youngs_std:.2f} ({log_rel_youngs*100:.2f}%)':<35}"
+ )
+
+ # Density
+ disp_density = val_metrics["val_abs_error_density"]
+ disp_density_std = val_metrics["val_abs_error_density_std"]
+ rel_density = val_metrics["val_rel_error_density"]
+ rel_density_std = val_metrics["val_rel_error_density_std"]
+ print(
+ f"{'Density':<20} {f'{disp_density:.4f}ยฑ{disp_density_std:.2f}':<35} {f'{rel_density:.4f}ยฑ{rel_density_std:.2f} ({rel_density*100:.2f}%)':<35}"
+ )
+
+ # Poisson's ratio
+ disp_poisson = val_metrics["val_abs_error_poisson"]
+ disp_poisson_std = val_metrics["val_abs_error_poisson_std"]
+ rel_poisson = val_metrics["val_rel_error_poisson"]
+ rel_poisson_std = val_metrics["val_rel_error_poisson_std"]
+ print(
+ f"{'Poissons Ratio':<20} {f'{disp_poisson:.4f}ยฑ{disp_poisson_std:.2f}':<35} {f'{rel_poisson:.4f}ยฑ{rel_poisson_std:.2f} ({rel_poisson*100:.2f}%)':<35}"
+ )
+
+ print("-" * 100)
+
+ # Table 2: Per-object averages
+ print("\nTable 2: PER-OBJECT AVERAGES (errors averaged per voxel, then per object)")
+ print("-" * 100)
+ print(
+ f"{'Property':<20} {'Per-Obj Displacement Error ยฑ Std':<35} {'Per-Obj Relative Error ยฑ Std':<35}"
+ )
+ print("-" * 100)
+
+ # Young's modulus (log space) - per object
+ per_obj_abs_youngs = val_metrics["per_object_abs_error_log_youngs_mean"]
+ per_obj_abs_youngs_std = val_metrics["per_object_abs_error_log_youngs_std"]
+ per_obj_rel_youngs = val_metrics["per_object_rel_error_log_youngs_mean"]
+ per_obj_rel_youngs_std = val_metrics["per_object_rel_error_log_youngs_std"]
+ print(
+ f"{'Youngs Modulus':<20} {f'{per_obj_abs_youngs:.4f}ยฑ{per_obj_abs_youngs_std:.2f} (log)':<35} {f'{per_obj_rel_youngs:.4f}ยฑ{per_obj_rel_youngs_std:.2f} ({per_obj_rel_youngs*100:.2f}%)':<35}"
+ )
+
+ # Density - per object
+ per_obj_abs_density = val_metrics["per_object_abs_error_density_mean"]
+ per_obj_abs_density_std = val_metrics["per_object_abs_error_density_std"]
+ per_obj_rel_density = val_metrics["per_object_rel_error_density_mean"]
+ per_obj_rel_density_std = val_metrics["per_object_rel_error_density_std"]
+ print(
+ f"{'Density':<20} {f'{per_obj_abs_density:.4f}ยฑ{per_obj_abs_density_std:.2f}':<35} {f'{per_obj_rel_density:.4f}ยฑ{per_obj_rel_density_std:.2f} ({per_obj_rel_density*100:.2f}%)':<35}"
+ )
+
+ # Poisson's ratio - per object
+ per_obj_abs_poisson = val_metrics["per_object_abs_error_poisson_mean"]
+ per_obj_abs_poisson_std = val_metrics["per_object_abs_error_poisson_std"]
+ per_obj_rel_poisson = val_metrics["per_object_rel_error_poisson_mean"]
+ per_obj_rel_poisson_std = val_metrics["per_object_rel_error_poisson_std"]
+ print(
+ f"{'Poissons Ratio':<20} {f'{per_obj_abs_poisson:.4f}ยฑ{per_obj_abs_poisson_std:.2f}':<35} {f'{per_obj_rel_poisson:.4f}ยฑ{per_obj_rel_poisson_std:.2f} ({per_obj_rel_poisson*100:.2f}%)':<35}"
+ )
+
+ print("=" * 100)
+
+ results_to_save = {
+ k: v
+ for k, v in val_metrics.items()
+ if k not in ["per_object_errors", "object_identifiers", "per_object_voxel_data"]
+ }
+ results_file = os.path.join(args.checkpoint_dir, "evaluation_results.json")
+ with open(results_file, "w") as f:
+ json.dump(results_to_save, f, indent=4)
+ print(f"Results saved to: {results_file}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/scripts/material_hist.py b/deps/vomp/scripts/material_hist.py
new file mode 100644
index 0000000000000000000000000000000000000000..828dcdd64d36c9d2882d30162a9119e8f024b9da
--- /dev/null
+++ b/deps/vomp/scripts/material_hist.py
@@ -0,0 +1,308 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import json
+import torch
+import numpy as np
+import pandas as pd
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import seaborn as sns
+from pathlib import Path
+from scipy import stats
+from matplotlib.ticker import MultipleLocator, FixedLocator
+import warnings
+
+warnings.filterwarnings("ignore")
+
+
+def create_individual_histogram_plots():
+ print("Loading materials data...")
+
+ df = pd.read_csv("../../datasets/latent_space/materials_filtered.csv")
+
+ E_values = df["youngs_modulus"].values
+ nu_values = df["poisson_ratio"].values
+ rho_values = df["density"].values
+
+ print(f"Loaded {len(df)} materials")
+ print(f"Young's Modulus range: {np.min(E_values):.2e} - {np.max(E_values):.2e} Pa")
+ print(f"Poisson's Ratio range: {np.min(nu_values):.3f} - {np.max(nu_values):.3f}")
+ print(f"Density range: {np.min(rho_values):.1f} - {np.max(rho_values):.1f} kg/mยณ")
+
+ output_dir = Path("outputs")
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ plt.rcdefaults()
+ plt.rcParams["font.family"] = "serif"
+ plt.rcParams["font.serif"] = ["Times New Roman", "DejaVu Serif"]
+ fontsize = 13
+ plt.rcParams["font.size"] = fontsize
+
+ fig, ax = plt.subplots(figsize=(6, 4))
+
+ ax.set_facecolor("#f2f2f2")
+ fig.patch.set_facecolor("white")
+
+ log_E = np.log10(E_values)
+
+ bins = np.linspace(log_E.min(), log_E.max(), 51)
+ counts, bin_edges = np.histogram(log_E, bins=bins)
+ total = len(log_E)
+ probabilities = counts / total
+
+ width = bins[1] - bins[0]
+ ax.bar(
+ bin_edges[:-1],
+ probabilities,
+ width=width,
+ color="#8AB6D6",
+ edgecolor="white",
+ linewidth=0.5,
+ alpha=0.9,
+ )
+
+ kde_x = np.linspace(log_E.min(), log_E.max(), 1000)
+ kde = sns.kdeplot(
+ log_E,
+ ax=ax,
+ color="#5A9BCF",
+ linewidth=1.5,
+ alpha=0.7,
+ clip=(log_E.min(), log_E.max()),
+ )
+
+ line = kde.get_lines()[-1]
+ kde_y = line.get_ydata()
+ if max(kde_y) > 0:
+ scale_factor = max(probabilities) / max(kde_y)
+ line.set_ydata(kde_y * scale_factor)
+
+ ax.set_xlim(log_E.min(), log_E.max())
+ ax.set_xlabel("Young's Modulus (log Pa)", fontsize=fontsize)
+ ax.set_ylabel("")
+
+ y_max = max(probabilities) * 1.1 if max(probabilities) > 0 else 1.0
+ ax.set_ylim(0, y_max)
+
+ ytick_values = np.arange(0.04, y_max, 0.04)
+ ax.yaxis.set_major_locator(FixedLocator(ytick_values))
+ ax.set_yticklabels([f"{y:.2f}" for y in ytick_values])
+
+ ax.yaxis.set_visible(True)
+ ax.spines["left"].set_visible(True)
+ ax.spines["left"].set_color("black")
+ ax.tick_params(axis="y", which="both", left=True, colors="black", length=4, width=1)
+
+ ax.text(
+ -0.08,
+ 1.01,
+ "Probability",
+ transform=ax.transAxes,
+ ha="left",
+ va="bottom",
+ fontsize=fontsize,
+ )
+
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+
+ ax.grid(True, axis="y", linestyle="-", color="white", linewidth=1.0)
+ ax.set_axisbelow(True)
+
+ plt.tight_layout()
+
+ output_path = output_dir / "youngs_modulus_histogram.pdf"
+ plt.savefig(output_path, dpi=300, format="pdf", bbox_inches="tight")
+ print(f"Saved Young's Modulus histogram to {output_path}")
+ plt.close()
+
+ plt.rcdefaults()
+ plt.rcParams["font.family"] = "serif"
+ plt.rcParams["font.serif"] = ["Times New Roman", "DejaVu Serif"]
+ fontsize = 13
+ plt.rcParams["font.size"] = fontsize
+
+ fig, ax = plt.subplots(figsize=(6, 4))
+
+ ax.set_facecolor("#f2f2f2")
+ fig.patch.set_facecolor("white")
+
+ bins = np.linspace(nu_values.min(), nu_values.max(), 51)
+ counts, bin_edges = np.histogram(nu_values, bins=bins)
+ total = len(nu_values)
+ probabilities = counts / total
+
+ width = bins[1] - bins[0]
+ ax.bar(
+ bin_edges[:-1],
+ probabilities,
+ width=width,
+ color="#8AB6D6",
+ edgecolor="white",
+ linewidth=0.5,
+ alpha=0.9,
+ )
+
+ kde_x = np.linspace(nu_values.min(), nu_values.max(), 1000)
+ kde = sns.kdeplot(
+ nu_values,
+ ax=ax,
+ color="#5A9BCF",
+ linewidth=1.5,
+ alpha=0.7,
+ clip=(nu_values.min(), nu_values.max()),
+ )
+
+ line = kde.get_lines()[-1]
+ kde_y = line.get_ydata()
+ if max(kde_y) > 0:
+ scale_factor = max(probabilities) / max(kde_y)
+ line.set_ydata(kde_y * scale_factor)
+
+ ax.set_xlim(nu_values.min(), nu_values.max())
+ ax.set_xlabel("Poisson's Ratio", fontsize=fontsize)
+ ax.set_ylabel("")
+
+ y_max = max(probabilities) * 1.1 if max(probabilities) > 0 else 1.0
+ ax.set_ylim(0, y_max)
+
+ ytick_values = np.arange(0.02, y_max, 0.02)
+ ax.yaxis.set_major_locator(FixedLocator(ytick_values))
+ ax.set_yticklabels([f"{y:.2f}" for y in ytick_values])
+
+ ax.yaxis.set_visible(True)
+ ax.spines["left"].set_visible(True)
+ ax.spines["left"].set_color("black")
+ ax.tick_params(axis="y", which="both", left=True, colors="black", length=4, width=1)
+
+ ax.text(
+ -0.08,
+ 1.01,
+ "Probability",
+ transform=ax.transAxes,
+ ha="left",
+ va="bottom",
+ fontsize=fontsize,
+ )
+
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+
+ ax.grid(True, axis="y", linestyle="-", color="white", linewidth=1.0)
+ ax.set_axisbelow(True)
+
+ plt.tight_layout()
+
+ output_path = output_dir / "poisson_ratio_histogram.pdf"
+ plt.savefig(output_path, dpi=300, format="pdf", bbox_inches="tight")
+ print(f"Saved Poisson's Ratio histogram to {output_path}")
+ plt.close()
+
+ plt.rcdefaults()
+ plt.rcParams["font.family"] = "serif"
+ plt.rcParams["font.serif"] = ["Times New Roman", "DejaVu Serif"]
+ fontsize = 13
+ plt.rcParams["font.size"] = fontsize
+
+ fig, ax = plt.subplots(figsize=(6, 4))
+
+ ax.set_facecolor("#f2f2f2")
+ fig.patch.set_facecolor("white")
+
+ log_rho = np.log10(rho_values)
+
+ bins = np.linspace(log_rho.min(), log_rho.max(), 51)
+ counts, bin_edges = np.histogram(log_rho, bins=bins)
+ total = len(log_rho)
+ probabilities = counts / total
+
+ width = bins[1] - bins[0]
+ ax.bar(
+ bin_edges[:-1],
+ probabilities,
+ width=width,
+ color="#8AB6D6",
+ edgecolor="white",
+ linewidth=0.5,
+ alpha=0.9,
+ )
+
+ kde_x = np.linspace(log_rho.min(), log_rho.max(), 1000)
+ kde = sns.kdeplot(
+ log_rho,
+ ax=ax,
+ color="#5A9BCF",
+ linewidth=1.5,
+ alpha=0.7,
+ clip=(log_rho.min(), log_rho.max()),
+ )
+
+ line = kde.get_lines()[-1]
+ kde_y = line.get_ydata()
+ if max(kde_y) > 0:
+ scale_factor = max(probabilities) / max(kde_y)
+ line.set_ydata(kde_y * scale_factor)
+
+ ax.set_xlim(log_rho.min(), log_rho.max())
+ ax.set_xlabel("Density (log kg/mยณ)", fontsize=fontsize)
+ ax.set_ylabel("")
+
+ y_max = max(probabilities) * 1.1 if max(probabilities) > 0 else 1.0
+ ax.set_ylim(0, y_max)
+
+ ytick_values = np.arange(0.04, y_max, 0.04)
+ ax.yaxis.set_major_locator(FixedLocator(ytick_values))
+ ax.set_yticklabels([f"{y:.2f}" for y in ytick_values])
+
+ ax.yaxis.set_visible(True)
+ ax.spines["left"].set_visible(True)
+ ax.spines["left"].set_color("black")
+ ax.tick_params(axis="y", which="both", left=True, colors="black", length=4, width=1)
+
+ ax.text(
+ -0.08,
+ 1.01,
+ "Probability",
+ transform=ax.transAxes,
+ ha="left",
+ va="bottom",
+ fontsize=fontsize,
+ )
+
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+
+ ax.grid(True, axis="y", linestyle="-", color="white", linewidth=1.0)
+ ax.set_axisbelow(True)
+
+ plt.tight_layout()
+
+ output_path = output_dir / "density_histogram.pdf"
+ plt.savefig(output_path, dpi=300, format="pdf", bbox_inches="tight")
+ print(f"Saved Density histogram to {output_path}")
+ plt.close()
+
+ return E_values, nu_values, rho_values
+
+
+if __name__ == "__main__":
+ print("Creating individual material property histograms...")
+ create_individual_histogram_plots()
+
+ print("\nHistogram generation complete!")
diff --git a/deps/vomp/scripts/optimize_prompt.py b/deps/vomp/scripts/optimize_prompt.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9c41e4f75c5f6222e9574551ea93a74138ca60b
--- /dev/null
+++ b/deps/vomp/scripts/optimize_prompt.py
@@ -0,0 +1,298 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This is the textgrad script for vlm annotation task. This requires installing textgrad."""
+
+OPENAI_API_KEY = "" # fill in your OpenAI API key
+ANNOTATIONS_FILE = "/home/rdagli/code/TRELLIS/datasets/raw/material_annotations.json" # fill in the path to the annotations file
+OUTPUT_DIR = "./optimization_results"
+
+import os
+import sys
+import json
+import random
+import numpy as np
+import torch
+from tqdm import tqdm
+from datetime import datetime
+import textgrad as tg
+from textgrad.engine import EngineLM
+
+os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
+
+from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
+ parse_vlm_properties,
+)
+from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration
+from qwen_vl_utils import process_vision_info
+
+tg.set_backward_engine("gpt-4o-mini")
+
+import logging
+
+timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+OUTPUT_DIR = f"{OUTPUT_DIR}_{timestamp}"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ handlers=[
+ logging.FileHandler(f"{OUTPUT_DIR}/optimization.log"),
+ logging.StreamHandler(),
+ ],
+)
+logger = logging.getLogger(__name__)
+logging.getLogger("textgrad").setLevel(logging.WARNING)
+
+
+class QwenEngine(EngineLM):
+ def __init__(self, model_id="Qwen/Qwen2.5-VL-72B-Instruct"):
+ self.model_string = model_id
+ logger.info(f"Loading {model_id}...")
+
+ self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
+ model_id,
+ torch_dtype=torch.bfloat16,
+ device_map="auto",
+ attn_implementation="flash_attention_2",
+ )
+
+ self.processor = AutoProcessor.from_pretrained(
+ model_id, min_pixels=256 * 28 * 28, max_pixels=1280 * 28 * 28
+ )
+ logger.info("Model loaded successfully")
+
+ def generate(self, prompt, system_prompt=None, **kwargs):
+ """
+ TextGrad EngineLM interface.
+ prompt: The input prompt (textgrad Variable value or string)
+ system_prompt: The system prompt (textgrad Variable value or string)
+ """
+
+ try:
+ if isinstance(prompt, str) and prompt.startswith("{"):
+ data = json.loads(prompt)
+ material_type = data.get("material_type")
+ semantic_usage = data.get("semantic_usage")
+ images = data.get("images", [])
+ else:
+ material_type = prompt
+ semantic_usage = None
+ images = []
+ except:
+ material_type = prompt
+ semantic_usage = None
+ images = []
+
+ intro_text = """
+You are a materials science expert analyzing an image of the full object (showing how the material appears in context).
+
+Using this image and the information below, identify the real-world material and estimate its mechanical properties.
+"""
+
+ user_prompt = f"""{intro_text}
+Material context:
+ * Material type: {material_type}
+ * Usage: {semantic_usage or material_type}
+
+Your task is to provide three specific properties:
+1. Young's modulus (in Pa using scientific notation)
+2. Poisson's ratio (a value between 0.0 and 0.5)
+3. Density (in kg/m^3 using scientific notation)
+
+Based on the provided images and context information, analyze the material properties.
+Note: The material segment might be internal to the object and not visible from the outside.
+
+Respond using EXACTLY the following format (do not deviate from this structure):
+
+Analysis:
+Step 1: Identify the material class/type based on visual appearance
+Step 2: Describe the surface characteristics (texture, reflectivity, color)
+Step 3: Determine the specific material subtype considering its physical properties
+Step 4: Reason through each property estimate based on visual and measured data
+
+Young's modulus: Pa
+Poisson's ratio:
+Density: kg/m^3
+
+Critical Instructions:
+1. You MUST provide numerical estimates for ALL materials, including organic or unusual materials
+2. For natural materials like leaves, wood, or paper, provide estimates based on similar materials with known properties
+3. Never use "N/A", "unknown", or any non-numeric responses for the material properties
+4. For Poisson's ratio, provide a simple decimal number (like 0.3 or 0.42)
+5. Each property should be on its own line with exact formatting shown above
+"""
+
+ user_content = []
+ if images:
+ for img_path in images:
+ if img_path and os.path.exists(img_path):
+ user_content.append(
+ {
+ "type": "image",
+ "image": f"file://{os.path.abspath(img_path)}",
+ }
+ )
+
+ user_content.append({"type": "text", "text": user_prompt})
+
+ messages = [
+ {
+ "role": "system",
+ "content": (
+ system_prompt if system_prompt else "You are a helpful assistant."
+ ),
+ },
+ {"role": "user", "content": user_content},
+ ]
+
+ text = self.processor.apply_chat_template(
+ messages, tokenize=False, add_generation_prompt=True
+ )
+
+ image_inputs, video_inputs = process_vision_info(messages)
+ inputs = self.processor(
+ text=[text], images=image_inputs, videos=video_inputs, return_tensors="pt"
+ ).to(self.model.device)
+
+ with torch.no_grad():
+ generated_ids = self.model.generate(**inputs, max_new_tokens=512)
+ generated_ids_trimmed = [
+ out_ids[len(in_ids) :]
+ for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
+ ]
+ output = self.processor.batch_decode(
+ generated_ids_trimmed,
+ skip_special_tokens=True,
+ clean_up_tokenization_spaces=False,
+ )[0]
+
+ return output
+
+ def __call__(self, prompt, system_prompt=None):
+ return self.generate(prompt, system_prompt)
+
+
+def main():
+
+ with open(ANNOTATIONS_FILE, "r") as f:
+ annotations = json.load(f)
+
+ samples = []
+ max_objects = 23
+ # rest 7 will serve as validation set
+ for obj in annotations[:max_objects]:
+ for seg_name, seg_info in obj.get("segments", {}).items():
+ if all(
+ k in seg_info for k in ["youngs_modulus", "poissons_ratio", "density"]
+ ):
+ samples.append(
+ {
+ "name": f"{obj['object_name']}/{seg_name}",
+ "material_type": seg_info.get("material_type", "unknown"),
+ "semantic_usage": seg_info.get("semantic_usage", seg_name),
+ "ground_truth": {
+ "youngs_modulus": float(seg_info["youngs_modulus"]),
+ "poissons_ratio": float(seg_info["poissons_ratio"]),
+ "density": float(seg_info["density"]),
+ },
+ }
+ )
+
+ logger.info(f"Loaded {len(samples)} samples")
+ random.shuffle(samples)
+
+ qwen_engine = QwenEngine()
+ initial_prompt = """You are a materials science expert. Help analyze material properties from images and context."""
+
+ system_prompt_var = tg.Variable(
+ initial_prompt,
+ requires_grad=True,
+ role_description="system prompt for material property prediction",
+ )
+
+ model = tg.BlackboxLLM(qwen_engine, system_prompt_var)
+
+ optimizer = tg.TextualGradientDescent(
+ engine=tg.get_engine("gpt-4o-mini"), parameters=[system_prompt_var]
+ )
+
+ eval_prompt = """Evaluate this material property prediction for:
+1. Format correctness: Are Young's modulus (in Pa), Poisson's ratio (0.0-0.5), and density (in kg/mยณ) all present?
+2. Value reasonableness: Are the magnitudes appropriate for the material type?
+3. Scientific notation: Is Young's modulus in proper scientific notation (e.g., 2.0e11 Pa)?
+
+Provide concise, specific feedback on what's missing or incorrect."""
+ loss_fn = tg.TextLoss(eval_prompt)
+
+ n_epochs = 25
+ batch_size = 3
+ metrics = {"loss": [], "prompts": []}
+
+ for epoch in range(n_epochs):
+ logger.info(f"\nEpoch {epoch + 1}/{n_epochs}")
+
+ optimizer.zero_grad()
+
+ for i in tqdm(range(0, len(samples), batch_size), desc="Training"):
+ batch = samples[i : i + batch_size]
+ optimizer.zero_grad()
+
+ batch_losses = []
+
+ for sample in batch:
+ input_data = json.dumps(
+ {
+ "material_type": sample["material_type"],
+ "semantic_usage": sample.get("semantic_usage"),
+ }
+ )
+
+ input_var = tg.Variable(
+ input_data,
+ requires_grad=False,
+ role_description="input material info",
+ )
+
+ prediction = model(input_var)
+
+ loss = loss_fn(prediction)
+ batch_losses.append(loss)
+
+ parsed = parse_vlm_properties(prediction.value)
+ gt = sample["ground_truth"]
+
+ logger.info(f" {sample['name']}:")
+ logger.info(f" Pred: {parsed}")
+ logger.info(f" GT: {gt}")
+
+ if batch_losses:
+ total_loss = tg.sum(batch_losses)
+ total_loss.backward()
+
+ try:
+ optimizer.step()
+ logger.info(" Optimizer step successful")
+ except Exception as e:
+ logger.warning(f" Optimizer failed: {e}")
+
+ with open(f"{OUTPUT_DIR}/final_prompt.txt", "w") as f:
+ f.write(system_prompt_var.value)
+ logger.info(f"Saved to {OUTPUT_DIR}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/scripts/viewer.py b/deps/vomp/scripts/viewer.py
new file mode 100644
index 0000000000000000000000000000000000000000..38cdf2cd487ba8b70b6d5721deedd28d8ae63e80
--- /dev/null
+++ b/deps/vomp/scripts/viewer.py
@@ -0,0 +1,634 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+import polyscope as ps
+import argparse
+import sys
+import os
+import json
+import matplotlib
+
+matplotlib.use("Agg") # Use non-interactive backend before importing pyplot
+import matplotlib.pyplot as plt
+import matplotlib.colors as mcolors
+from matplotlib.colorbar import ColorbarBase
+
+
+def load_npz_file(filename):
+ """Load an npz file and extract point cloud data."""
+ try:
+ # Load the npz file
+ npz_data = np.load(filename)
+ print(f"Successfully loaded {filename}")
+ print(f"Available arrays in the file: {list(npz_data.files)}")
+
+ # Dictionary to store all point clouds found
+ point_clouds = {}
+
+ # Iterate through all arrays in the npz file
+ for array_name in npz_data.files:
+ array_data = npz_data[array_name]
+ print(
+ f"\nArray '{array_name}': shape={array_data.shape}, dtype={array_data.dtype}"
+ )
+
+ # Check if this is a structured array with coordinate fields
+ if array_data.dtype.names is not None:
+ print(f" Structured array with fields: {array_data.dtype.names}")
+
+ # Look for coordinate fields
+ coord_fields = []
+ if "x" in array_data.dtype.names and "y" in array_data.dtype.names:
+ coord_fields = ["x", "y"]
+ if "z" in array_data.dtype.names:
+ coord_fields.append("z")
+
+ # Extract coordinates
+ if len(coord_fields) == 2:
+ # 2D coordinates
+ points = np.column_stack(
+ [array_data[field] for field in coord_fields]
+ )
+ # Add z=0 to make 3D
+ points_3d = np.column_stack([points, np.zeros(points.shape[0])])
+ point_clouds[array_name] = points_3d.astype(np.float32)
+ print(
+ f" -> Extracted 2D coordinates from fields {coord_fields} (converted to 3D)"
+ )
+ else:
+ # 3D coordinates
+ points = np.column_stack(
+ [array_data[field] for field in coord_fields]
+ )
+ point_clouds[array_name] = points.astype(np.float32)
+ print(
+ f" -> Extracted 3D coordinates from fields {coord_fields}"
+ )
+
+ print(f" -> Found {points.shape[0]} points")
+
+ # Store additional scalar data as attributes
+ additional_data = {}
+ for field_name in array_data.dtype.names:
+ if field_name not in coord_fields:
+ field_data = array_data[field_name]
+ # Only add numeric fields as scalar quantities
+ if np.issubdtype(field_data.dtype, np.number):
+ additional_data[field_name] = field_data
+ print(
+ f" -> Will add '{field_name}' as scalar quantity"
+ )
+
+ # Store additional data with the points
+ if additional_data:
+ point_clouds[array_name + "_metadata"] = additional_data
+ else:
+ print(f" -> No coordinate fields (x, y) found in structured array")
+
+ # Check if this could be a regular point cloud (should be Nx2 or Nx3)
+ elif len(array_data.shape) == 2:
+ if array_data.shape[1] == 2:
+ # 2D points - add z=0 to make them 3D
+ points_3d = np.column_stack(
+ [array_data, np.zeros(array_data.shape[0])]
+ )
+ point_clouds[array_name] = points_3d.astype(np.float32)
+ print(f" -> Added as 2D point cloud (converted to 3D)")
+ elif array_data.shape[1] == 3:
+ # 3D points
+ point_clouds[array_name] = array_data.astype(np.float32)
+ print(f" -> Added as 3D point cloud")
+ elif array_data.shape[1] > 3:
+ # Use only first 3 columns as XYZ coordinates
+ points_3d = array_data[:, :3]
+ point_clouds[array_name] = points_3d.astype(np.float32)
+ print(f" -> Using first 3 columns as 3D point cloud")
+ else:
+ print(
+ f" -> Skipped (only {array_data.shape[1]} column(s), need at least 2)"
+ )
+ elif len(array_data.shape) == 1 and len(array_data) >= 2:
+ # 1D array with at least 2 elements - treat as single point
+ if len(array_data) == 2:
+ # 2D point
+ point_clouds[array_name] = np.array(
+ [[array_data[0], array_data[1], 0.0]], dtype=np.float32
+ )
+ print(f" -> Added as single 2D point (converted to 3D)")
+ elif len(array_data) >= 3:
+ # 3D point (use first 3 elements)
+ point_clouds[array_name] = np.array(
+ [[array_data[0], array_data[1], array_data[2]]],
+ dtype=np.float32,
+ )
+ print(f" -> Added as single 3D point")
+ else:
+ print(f" -> Skipped (incompatible shape for point cloud)")
+
+ npz_data.close()
+
+ actual_point_clouds_data = []
+ for name, data in point_clouds.items():
+ if (
+ not name.endswith("_metadata")
+ and isinstance(data, np.ndarray)
+ and len(data.shape) == 2
+ ):
+ actual_point_clouds_data.append(data)
+
+ if actual_point_clouds_data:
+ # Compute overall bounding box across all point clouds
+ all_points = np.vstack(actual_point_clouds_data)
+ min_coords = np.min(all_points, axis=0)
+ max_coords = np.max(all_points, axis=0)
+ coord_range = max_coords - min_coords
+ max_range = np.max(coord_range)
+
+ print(f"\nOriginal coordinate ranges:")
+ print(
+ f" Min: [{min_coords[0]:.6f}, {min_coords[1]:.6f}, {min_coords[2]:.6f}]"
+ )
+ print(
+ f" Max: [{max_coords[0]:.6f}, {max_coords[1]:.6f}, {max_coords[2]:.6f}]"
+ )
+ print(f" Max range: {max_range:.6f}")
+
+ # Normalize to [-0.5, 0.5] range to match the "ours" method scale
+ # This ensures camera views are consistent across methods
+ if max_range > 1e-10: # Avoid division by zero
+ print(f" -> Normalizing coordinates to [-0.5, 0.5] range")
+
+ # Calculate center and normalize around it
+ center = (min_coords + max_coords) / 2
+
+ # Normalize all point clouds to [-0.5, 0.5] range
+ for name, data in point_clouds.items():
+ if (
+ not name.endswith("_metadata")
+ and isinstance(data, np.ndarray)
+ and len(data.shape) == 2
+ ):
+ # Center the data and scale to [-0.5, 0.5]
+ centered_data = data - center
+ normalized_data = (
+ centered_data / max_range
+ ) # Scale to [-0.5, 0.5] approximately
+ point_clouds[name] = normalized_data.astype(np.float32)
+
+ # Print new ranges to verify
+ all_normalized_points = np.vstack(
+ [
+ data
+ for name, data in point_clouds.items()
+ if not name.endswith("_metadata")
+ and isinstance(data, np.ndarray)
+ and len(data.shape) == 2
+ ]
+ )
+ new_min = np.min(all_normalized_points, axis=0)
+ new_max = np.max(all_normalized_points, axis=0)
+ new_range = np.max(new_max - new_min)
+ print(
+ f" New Min: [{new_min[0]:.6f}, {new_min[1]:.6f}, {new_min[2]:.6f}]"
+ )
+ print(
+ f" New Max: [{new_max[0]:.6f}, {new_max[1]:.6f}, {new_max[2]:.6f}]"
+ )
+ print(f" New max range: {new_range:.6f}")
+ else:
+ print(f" -> Coordinates have no range, skipping normalization")
+
+ return point_clouds
+
+ except FileNotFoundError:
+ print(f"Error: File '{filename}' not found.")
+ return None
+ except Exception as e:
+ print(f"Error loading file '{filename}': {str(e)}")
+ return None
+
+
+def create_matplotlib_legend(data, property_name, colormap="viridis", filename=None):
+ """Create a matplotlib-style colorbar legend and save it."""
+ fig, ax = plt.subplots(figsize=(8, 1))
+ fig.subplots_adjust(bottom=0.5)
+
+ # Remove the main axis
+ ax.remove()
+
+ # Create colorbar
+ cmap = plt.cm.get_cmap(colormap)
+ norm = mcolors.Normalize(vmin=np.min(data), vmax=np.max(data))
+
+ # Create colorbar axis
+ cbar_ax = fig.add_axes([0.1, 0.3, 0.8, 0.4])
+ cb = ColorbarBase(cbar_ax, cmap=cmap, norm=norm, orientation="horizontal")
+ # Remove labels - no text on the colorbar
+ cb.set_label("") # Empty label
+
+ # Remove tick labels if you want completely clean
+ # cb.set_ticks([]) # Uncomment this line to also remove tick marks/numbers
+
+ # No title - clean colorbar only
+ # plt.suptitle("", fontsize=16, fontweight="bold") # Removed title
+
+ if filename is None:
+ filename = f"{property_name}_colorbar_legend.png"
+
+ plt.savefig(filename, dpi=300, bbox_inches="tight", facecolor="white")
+ plt.close()
+ print(f"Saved colorbar legend: {filename}")
+ return filename
+
+
+def visualize_points(
+ point_clouds,
+ show_individual=True,
+ show_combined=True,
+ camera_file=None,
+ headless=False,
+):
+ """Visualize point clouds using polyscope."""
+
+ # Initialize polyscope
+ ps.init()
+
+ # Set some nice viewing options
+ ps.set_up_dir("z_up") # Z axis pointing up
+ ps.set_ground_plane_mode("shadow_only") # Show shadows on ground plane
+
+ # Load camera view if provided
+ if camera_file:
+ try:
+ if os.path.exists(camera_file):
+ with open(camera_file, "r") as f:
+ camera_json_str = f.read() # Read the JSON string directly
+ ps.set_view_from_json(camera_json_str)
+ print(f"Loaded camera view from: {camera_file}")
+ else:
+ print(
+ f"Warning: Camera file '{camera_file}' not found, using default view"
+ )
+ except Exception as e:
+ print(f"Warning: Failed to load camera file '{camera_file}': {str(e)}")
+ print("Using default camera view")
+
+ if not point_clouds:
+ print("No valid point clouds found to visualize.")
+ return
+
+ # Separate point cloud data from metadata
+ actual_point_clouds = {}
+ metadata_dict = {}
+
+ for name, data in point_clouds.items():
+ if name.endswith("_metadata"):
+ # This is metadata for another point cloud
+ original_name = name[:-9] # Remove '_metadata' suffix
+ metadata_dict[original_name] = data
+ else:
+ # This is actual point cloud data
+ actual_point_clouds[name] = data
+
+ if not actual_point_clouds:
+ print("No valid point clouds found to visualize.")
+ return
+
+ print(f"\nVisualizing {len(actual_point_clouds)} point cloud(s)...")
+
+ # Properties to create separate visualizations for
+ # Handle different naming conventions across methods
+ target_properties = ["youngs_modulus", "poissons_ratio", "density"]
+
+ # Create mapping for alternative field names (Phys4DGen/PUGS use different names)
+ field_name_mapping = {
+ "youngs_modulus": ["youngs_modulus", "young_modulus"],
+ "poissons_ratio": ["poissons_ratio", "poisson_ratio"],
+ "density": ["density"],
+ }
+
+ # Create separate point clouds for each target property
+ for cloud_name, points in actual_point_clouds.items():
+ if cloud_name in metadata_dict:
+ metadata = metadata_dict[cloud_name]
+
+ for prop_name in target_properties:
+ # Find the actual field name in the metadata (handle different naming conventions)
+ actual_field_name = None
+ prop_data = None
+
+ for possible_name in field_name_mapping[prop_name]:
+ if possible_name in metadata:
+ actual_field_name = possible_name
+ prop_data = metadata[possible_name]
+ break
+
+ if prop_data is not None:
+ # Register point cloud for this property
+ ps_cloud = ps.register_point_cloud(
+ f"{prop_name}_visualization", points
+ )
+
+ # Add the property as a scalar quantity with viridis colormap
+ ps_cloud.add_scalar_quantity(
+ prop_name, prop_data, enabled=True, cmap="viridis"
+ )
+
+ # Hide the point cloud initially (we'll show them one by one)
+ ps_cloud.set_enabled(False)
+
+ print(
+ f" Created visualization for '{prop_name}' (field: '{actual_field_name}'): {points.shape[0]} points"
+ )
+
+ # Create matplotlib legend for this property (only create legend images)
+ create_matplotlib_legend(prop_data, prop_name, "viridis")
+ else:
+ print(
+ f" Warning: Property '{prop_name}' not found in metadata (tried: {field_name_mapping[prop_name]})"
+ )
+
+ # Enable all point clouds for interactive exploration
+ for prop_name in target_properties:
+ cloud_name = f"{prop_name}_visualization"
+ if ps.has_point_cloud(cloud_name):
+ ps.get_point_cloud(cloud_name).set_enabled(True)
+
+ # Add a slice plane for exploring internal structure
+ slice_plane = None
+ if actual_point_clouds:
+ # Calculate center point of all data
+ all_points = np.vstack(list(actual_point_clouds.values()))
+ center = np.mean(all_points, axis=0)
+
+ # Add slice plane through the center
+ slice_plane = ps.add_scene_slice_plane()
+ slice_plane.set_pose(
+ center, (1.0, 0.0, 0.0)
+ ) # Start with X-normal plane through center
+ slice_plane.set_draw_plane(True) # Show the semi-transparent plane
+ slice_plane.set_draw_widget(True) # Show interactive controls
+
+ print(f"Added slice plane at center: {center}")
+ print(f" - Use the widget to drag and rotate the slice plane")
+ print(f" - Or use View -> Slice Planes in the GUI to control it")
+
+ # Define callback functions for UI buttons
+ def capture_current_property():
+ """Capture and save the currently visible property visualizations."""
+ screenshots_taken = []
+
+ # Find which properties are currently enabled
+ enabled_properties = []
+ for prop_name in target_properties:
+ cloud_name = f"{prop_name}_visualization"
+ if (
+ ps.has_point_cloud(cloud_name)
+ and ps.get_point_cloud(cloud_name).is_enabled()
+ ):
+ enabled_properties.append(prop_name)
+
+ if not enabled_properties:
+ print("No property visualizations are currently visible!")
+ return
+
+ # Take screenshot of current view
+ screenshot_filename = f"captured_properties.png"
+ ps.screenshot(screenshot_filename)
+ screenshots_taken.append(screenshot_filename)
+
+ # Save camera view as JSON
+ camera_json_str = ps.get_view_as_json()
+ camera_filename = "camera.json"
+ with open(camera_filename, "w") as f:
+ f.write(camera_json_str) # Write the JSON string directly from polyscope
+
+ print(
+ f"Captured screenshot of properties {enabled_properties}: {screenshot_filename}"
+ )
+ print(f"Saved camera view: {camera_filename}")
+ return screenshots_taken
+
+ def capture_all_properties():
+ """Capture and save each property individually at current camera position."""
+ screenshots_taken = []
+
+ # Save camera view as JSON (once, since all screenshots use same camera position)
+ camera_json_str = ps.get_view_as_json()
+ camera_filename = "camera.json"
+ with open(camera_filename, "w") as f:
+ f.write(camera_json_str) # Write the JSON string directly from polyscope
+
+ # Store original slice plane state
+ original_slice_active = None
+ original_draw_plane = None
+ original_draw_widget = None
+ if slice_plane is not None:
+ original_slice_active = slice_plane.get_active()
+ original_draw_plane = slice_plane.get_draw_plane()
+ original_draw_widget = slice_plane.get_draw_widget()
+
+ # First pass: Capture images WITHOUT slice plane effects
+ if slice_plane is not None:
+ slice_plane.set_active(False) # Disable slice plane effects completely
+
+ for prop_name in target_properties:
+ cloud_name = f"{prop_name}_visualization"
+ if ps.has_point_cloud(cloud_name):
+ # Hide all point clouds first
+ for other_prop in target_properties:
+ other_cloud_name = f"{other_prop}_visualization"
+ if ps.has_point_cloud(other_cloud_name):
+ ps.get_point_cloud(other_cloud_name).set_enabled(False)
+
+ # Show only this property's point cloud
+ ps.get_point_cloud(cloud_name).set_enabled(True)
+
+ # Take screenshot without slice plane
+ screenshot_filename = f"{prop_name}_no_slice.png"
+ ps.screenshot(screenshot_filename)
+ screenshots_taken.append(screenshot_filename)
+ print(f"Captured {prop_name} (no slice): {screenshot_filename}")
+
+ # Second pass: Capture images WITH slice plane effects (but hide visuals)
+ if slice_plane is not None:
+ slice_plane.set_active(True) # Enable slice plane effects
+ slice_plane.set_draw_plane(False) # Hide the plane visual
+ slice_plane.set_draw_widget(False) # Hide the widget
+
+ for prop_name in target_properties:
+ cloud_name = f"{prop_name}_visualization"
+ if ps.has_point_cloud(cloud_name):
+ # Hide all point clouds first
+ for other_prop in target_properties:
+ other_cloud_name = f"{other_prop}_visualization"
+ if ps.has_point_cloud(other_cloud_name):
+ ps.get_point_cloud(other_cloud_name).set_enabled(False)
+
+ # Show only this property's point cloud
+ ps.get_point_cloud(cloud_name).set_enabled(True)
+
+ # Take screenshot with slice plane effects (but visuals hidden)
+ screenshot_filename = f"{prop_name}_with_slice.png"
+ ps.screenshot(screenshot_filename)
+ screenshots_taken.append(screenshot_filename)
+ print(f"Captured {prop_name} (with slice): {screenshot_filename}")
+
+ # Restore original slice plane state
+ if slice_plane is not None:
+ slice_plane.set_active(original_slice_active)
+ slice_plane.set_draw_plane(original_draw_plane)
+ slice_plane.set_draw_widget(original_draw_widget)
+
+ # Re-enable all point clouds
+ for prop_name in target_properties:
+ cloud_name = f"{prop_name}_visualization"
+ if ps.has_point_cloud(cloud_name):
+ ps.get_point_cloud(cloud_name).set_enabled(True)
+
+ print(
+ f"Captured {len(screenshots_taken)} property visualizations (6 total: 3 without slice, 3 with slice)"
+ )
+ print(f"Saved camera view: {camera_filename}")
+ return screenshots_taken
+
+ if headless:
+ # Headless mode: automatically capture images and exit
+ print(f"\nRunning in headless mode...")
+
+ # Print basic statistics
+ total_points = sum(points.shape[0] for points in actual_point_clouds.values())
+ print(f"Total points loaded: {total_points}")
+
+ if camera_file:
+ print(f"Using camera view from: {camera_file}")
+ else:
+ print(f"Using default camera view")
+
+ print(f"Capturing 6 images...")
+ capture_all_properties()
+
+ print(f"Headless mode complete. Generated files:")
+ for prop_name in target_properties:
+ print(f" - {prop_name}_no_slice.png")
+ print(f" - {prop_name}_with_slice.png")
+ print(f" - {prop_name}_colorbar_legend.png")
+ print(f" - camera.json")
+
+ else:
+ # Interactive mode
+ # Add UI callbacks
+ def callback():
+ if ps.imgui.Button("Capture Current View"):
+ capture_current_property()
+
+ if ps.imgui.Button("Capture All Properties"):
+ capture_all_properties()
+
+ ps.set_user_callback(callback)
+
+ # Print statistics
+ total_points = sum(points.shape[0] for points in actual_point_clouds.values())
+ print(f"\nTotal points loaded: {total_points}")
+
+ # Compute and print bounding box
+ if total_points > 0:
+ all_points = np.vstack(list(actual_point_clouds.values()))
+ min_coords = np.min(all_points, axis=0)
+ max_coords = np.max(all_points, axis=0)
+ print(f"Bounding box: min={min_coords}, max={max_coords}")
+
+ print(f"\nLegend files created:")
+ for prop_name in target_properties:
+ print(f" - {prop_name}_colorbar_legend.png (matplotlib colorbar)")
+
+ print(f"\nUse the buttons in the UI to capture visualizations:")
+ print(
+ f" - 'Capture Current View': Save current view with visible properties + camera.json"
+ )
+ print(
+ f" - 'Capture All Properties': Save 6 images (3 without slice, 3 with slice) + camera.json"
+ )
+
+ print(f"\nInteractive features:")
+ print(f" - Toggle point cloud visibility in the left panel")
+ print(f" - Drag the slice plane widget to explore internal structure")
+ print(f" - Use View -> Slice Planes in the menu for more slice plane controls")
+
+ if camera_file:
+ print(f"\nStarted with camera view from: {camera_file}")
+ else:
+ print(
+ f"\nStarted with default camera view (use --cam to load a saved view)"
+ )
+
+ ps.show()
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Visualize point clouds from an NPZ file using polyscope"
+ )
+ parser.add_argument("filename", help="Path to the NPZ file to load")
+ parser.add_argument(
+ "--no-individual",
+ action="store_true",
+ help="Don't show individual point clouds with different colors",
+ )
+ parser.add_argument(
+ "--no-combined",
+ action="store_true",
+ help="Don't show combined view of all point clouds",
+ )
+ parser.add_argument(
+ "--cam",
+ type=str,
+ help="Path to camera.json file to load initial camera view from",
+ )
+ parser.add_argument(
+ "--headless",
+ action="store_true",
+ help="Run in headless mode: automatically capture 6 images and exit (no GUI)",
+ )
+
+ args = parser.parse_args()
+
+ # Check if file exists
+ if not os.path.exists(args.filename):
+ print(f"Error: File '{args.filename}' does not exist.")
+ sys.exit(1)
+
+ # Load the NPZ file
+ point_clouds = load_npz_file(args.filename)
+
+ if point_clouds is None:
+ sys.exit(1)
+
+ if not point_clouds:
+ print("No valid point clouds found in the file.")
+ sys.exit(1)
+
+ # Visualize the points
+ show_individual = not args.no_individual
+ show_combined = not args.no_combined
+
+ visualize_points(
+ point_clouds, show_individual, show_combined, args.cam, args.headless
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/simulation/libuipc.md b/deps/vomp/simulation/libuipc.md
new file mode 100644
index 0000000000000000000000000000000000000000..fcff28d3d7a5ba6ce8152114b6424ea3a2922096
--- /dev/null
+++ b/deps/vomp/simulation/libuipc.md
@@ -0,0 +1 @@
+libuipc simulation code is present in the [`vomp/sim/`](../vomp/sim/) directory. Refer to the [README](../README.md) for instructions on how to run them.
\ No newline at end of file
diff --git a/deps/vomp/simulation/newton/mesh_falling_sim.py b/deps/vomp/simulation/newton/mesh_falling_sim.py
new file mode 100755
index 0000000000000000000000000000000000000000..26a124aa8cc380dfe99d0bcab326bb1825374582
--- /dev/null
+++ b/deps/vomp/simulation/newton/mesh_falling_sim.py
@@ -0,0 +1,261 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import numpy as np
+import warp as wp
+
+import newton
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="High-res soft cube fall with SemiImplicit solver"
+ )
+ parser.add_argument(
+ "--grid_dim",
+ type=int,
+ default=48,
+ help="Grid cells per axis (higher -> slower, more accurate)",
+ )
+ parser.add_argument(
+ "--cell_size", type=float, default=0.1, help="Cell size (meters)"
+ )
+ parser.add_argument(
+ "--drop_height", type=float, default=1.0, help="Drop height (meters)"
+ )
+ parser.add_argument("--fps", type=int, default=60, help="Viewer FPS")
+ parser.add_argument(
+ "--duration",
+ type=float,
+ default=3.0,
+ help="Seconds to simulate before auto-restart",
+ )
+ parser.add_argument(
+ "--substeps",
+ type=int,
+ default=256,
+ help="Substeps per frame (smaller dt; higher -> slower)",
+ )
+ parser.add_argument(
+ "--youngs_modulus",
+ type=float,
+ default=8000.0,
+ help="Young's modulus (stiffness)",
+ )
+ parser.add_argument(
+ "--poisson_ratio", type=float, default=0.45, help="Poisson's ratio (0-0.5)"
+ )
+ parser.add_argument(
+ "--density", type=float, default=200.0, help="Material volumetric density"
+ )
+ parser.add_argument(
+ "--materials",
+ type=str,
+ default=None,
+ help="Path to NPZ with per-tet materials (E, nu, density)",
+ )
+
+ args = parser.parse_args()
+
+ wp.init()
+
+ fps = args.fps
+ frame_dt = 1.0 / fps
+ sim_substeps = max(1, args.substeps)
+ sim_dt = frame_dt / sim_substeps
+ total_frames = int(args.duration * fps)
+
+ # Material (Lamรฉ)
+ E = args.youngs_modulus
+ nu = args.poisson_ratio
+ k_mu = 0.5 * E / (1.0 + nu)
+ k_lambda = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
+ k_damp = 25.0
+
+ print(f"Material: E={E:.1f}, nu={nu:.3f} -> mu={k_mu:.2f}, lambda={k_lambda:.2f}")
+
+ builder = newton.ModelBuilder()
+
+ builder.add_ground_plane(
+ cfg=newton.ModelBuilder.ShapeConfig(ke=5.0e3, kd=50.0, mu=0.6)
+ )
+
+ dim = args.grid_dim
+ cell = args.cell_size
+
+ print(f"Grid: dim={dim}, cell={cell} m, cube extentโ{dim*cell:.3f} m")
+
+ builder.default_particle_radius = cell * 0.5
+
+ # this will be overridden by the materials file
+ particle_density = float(args.density)
+
+ builder.add_soft_grid(
+ pos=wp.vec3(0.0, 0.0, float(args.drop_height)),
+ rot=wp.quat_identity(),
+ vel=wp.vec3(0.0, 0.0, 0.0),
+ dim_x=dim,
+ dim_y=dim,
+ dim_z=dim,
+ cell_x=cell,
+ cell_y=cell,
+ cell_z=cell,
+ density=particle_density,
+ k_mu=float(k_mu),
+ k_lambda=float(k_lambda),
+ k_damp=float(k_damp),
+ tri_ke=1e-3,
+ tri_ka=1e-3,
+ tri_kd=1e-4,
+ fix_bottom=False,
+ )
+
+ model = builder.finalize()
+ print(f"Model: particles={model.particle_count} tets={model.tet_count}")
+
+ # Soft contact parameters
+ model.soft_contact_ke = 5.0e3
+ model.soft_contact_kd = 50.0
+ model.soft_contact_mu = 0.6
+
+ # spatially varying tet materials and density
+ # See: https://newton-physics.github.io/newton/api/_generated/newton.Model.html#newton.Model.tet_materials
+ if args.materials:
+ try:
+ import numpy as _np
+ import warp as _wp
+
+ data = _np.load(args.materials)
+ tet_count = int(model.tet_count)
+
+ if "E" in data and "nu" in data:
+ E_arr = _np.asarray(data["E"]).reshape(-1)
+ nu_arr = _np.asarray(data["nu"]).reshape(-1)
+ if E_arr.shape[0] == tet_count and nu_arr.shape[0] == tet_count:
+ mu_arr = 0.5 * E_arr / (1.0 + nu_arr)
+ lam_arr = (E_arr * nu_arr) / ((1.0 + nu_arr) * (1.0 - 2.0 * nu_arr))
+ damp_arr = _np.full(tet_count, k_damp, dtype=_np.float32)
+ tet_mats = _np.stack([mu_arr, lam_arr, damp_arr], axis=1).astype(
+ _np.float32
+ )
+ model.tet_materials = _wp.array(tet_mats, dtype=_wp.float32)
+ else:
+ try:
+ est_dim = int(round(((E_arr.shape[0] / 5.0) ** (1.0 / 3.0))))
+ except Exception:
+ est_dim = -1
+ print(
+ f"[materials] E/nu length mismatch (got {E_arr.shape[0]}/{nu_arr.shape[0]}, need {tet_count}).\n"
+ f" Hint: NPZ likely built for grid_dimโ{est_dim}; current grid_dim={dim}."
+ )
+
+ rho_key = (
+ "density" if "density" in data else ("rho" if "rho" in data else None)
+ )
+ if rho_key is not None:
+ rho_arr = _np.asarray(data[rho_key]).reshape(-1)
+ if rho_arr.shape[0] == tet_count:
+ pos = state_0.particle_q.numpy()
+ tet_idx = model.tet_indices.numpy().reshape(-1, 4)
+
+ def tet_vol(a, b, c, d):
+ return (
+ abs(
+ _np.linalg.det(_np.stack([b - a, c - a, d - a], axis=1))
+ )
+ / 6.0
+ )
+
+ vols = _np.empty(tet_count, dtype=_np.float64)
+ for t in range(tet_count):
+ i, j, k, l = tet_idx[t]
+ vols[t] = tet_vol(pos[i], pos[j], pos[k], pos[l])
+ p_mass = _np.zeros(model.particle_count, dtype=_np.float64)
+ for t in range(tet_count):
+ i, j, k, l = tet_idx[t]
+ m = float(rho_arr[t]) * float(vols[t]) / 4.0
+ p_mass[i] += m
+ p_mass[j] += m
+ p_mass[k] += m
+ p_mass[l] += m
+ model.particle_mass = _wp.array(
+ p_mass.astype(_np.float32), dtype=_wp.float32
+ )
+ else:
+ try:
+ est_dim_rho = int(
+ round(((rho_arr.shape[0] / 5.0) ** (1.0 / 3.0)))
+ )
+ except Exception:
+ est_dim_rho = -1
+ print(
+ f"[materials] density length mismatch (got {rho_arr.shape[0]}, need {tet_count}).\n"
+ f" Hint: NPZ likely built for grid_dimโ{est_dim_rho}; current grid_dim={dim}."
+ )
+ except Exception as e:
+ print(f"[materials] failed to apply materials: {e}")
+
+ # SemiImplicit solver
+ solver = newton.solvers.SolverSemiImplicit(model)
+
+ state_0 = model.state()
+ state_1 = model.state()
+ control = model.control()
+
+ newton.eval_fk(model, model.joint_q, model.joint_qd, state_0)
+ contacts = model.collide(state_0, soft_contact_margin=0.01)
+
+ viewer = newton.viewer.ViewerGL(headless=False)
+ viewer.set_model(model)
+ print("Viewer: SPACE to pause/resume, ESC to quit")
+
+ sim_time = 0.0
+ frame = 0
+
+ while viewer.is_running():
+ if frame >= total_frames:
+ frame = 0
+ sim_time = 0.0
+ state_0 = model.state()
+ state_1 = model.state()
+ newton.eval_fk(model, model.joint_q, model.joint_qd, state_0)
+
+ if not viewer.is_paused():
+ for _ in range(sim_substeps):
+ state_0.clear_forces()
+ contacts = model.collide(state_0, soft_contact_margin=0.01)
+ solver.step(state_0, state_1, control, contacts, sim_dt)
+ state_0, state_1 = state_1, state_0
+
+ sim_time += frame_dt
+ frame += 1
+
+ if frame % fps == 0:
+ com = np.mean(state_0.particle_q.numpy(), axis=0)
+ print(
+ f"t={sim_time:.2f}s COM=({com[0]:.3f},{com[1]:.3f},{com[2]:.3f})"
+ )
+
+ viewer.begin_frame(sim_time)
+ viewer.log_state(state_0)
+ viewer.log_contacts(contacts, state_0)
+ viewer.end_frame()
+
+ viewer.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/simulation/simplicits.md b/deps/vomp/simulation/simplicits.md
new file mode 100644
index 0000000000000000000000000000000000000000..9446a3a3904926c09ca7ffa2b11cb553e6c9b8fa
--- /dev/null
+++ b/deps/vomp/simulation/simplicits.md
@@ -0,0 +1 @@
+Refer to the [Simplicits](https://kaolin.readthedocs.io/en/latest/notes/simplicits.html) documentation for instructions on how to run Simplicits simulations which are directly compatible with our properties.
\ No newline at end of file
diff --git a/deps/vomp/simulation/warp.fem/README.md b/deps/vomp/simulation/warp.fem/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2af51b31fe45645acf3a528a293754f52ea1d182
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/README.md
@@ -0,0 +1 @@
+fem_examples was written by Gilles Daviet.
\ No newline at end of file
diff --git a/deps/vomp/simulation/warp.fem/drop_tetmesh.py b/deps/vomp/simulation/warp.fem/drop_tetmesh.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c6ea077bc708bfb6161d4973750b499cf49e4fe
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/drop_tetmesh.py
@@ -0,0 +1,314 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Domain, Sample, Field
+from warp.fem import integrand, normal
+
+from fem_examples.mfem.softbody_sim import ClassicFEM, run_softbody_sim
+from fem_examples.mfem.variable_density import ClassicFEMWithDensity
+from fem_examples.mfem.collisions import CollisionHandler
+
+from fem_examples.mfem.mfem_3d import MFEM_RS_F, MFEM_sF_S
+
+import warp.examples.fem.utils as fem_example_utils
+import meshio
+import numpy as np
+
+from material_loader import (
+ apply_spatially_varying_materials,
+ visualize_material_distribution,
+ load_material_data,
+)
+from vomp.inference.utils import MaterialUpsampler
+
+
+@wp.func
+def material_fraction(x: wp.vec3):
+ return 1.0
+ # return wp.select(wp.length(x - wp.vec3(0.5, 1.0, 0.875)) > 0.2, 0.0, 1.0)
+
+
+@integrand
+def material_fraction_form(s: Sample, domain: Domain, phi: Field):
+ return material_fraction(domain(s)) * phi(s)
+
+
+@wp.kernel
+def mark_active(fraction: wp.array(dtype=wp.float64), active: wp.array(dtype=int)):
+ active[wp.tid()] = int(wp.nonzero(fraction[wp.tid()]))
+
+
+@integrand
+def clamped_edge(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ clamped = float(0.0)
+
+ # Single clamped edge
+ if s.qp_index < 10:
+ clamped = 1.0
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamped_right(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ pos = domain(s)
+ clamped = float(0.0)
+
+ # clamped right sides
+ clamped = 0.0 # wp.select(pos[0] < 1.0, 1.0, 0.0)
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamped_sides(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = normal(domain, s)
+ clamped = float(0.0)
+
+ # clamped vertical sides
+ clamped = wp.abs(nor[0])
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def boundary_displacement_form(
+ s: Sample,
+ domain: Domain,
+ v: Field,
+ displacement: float,
+):
+ """Prescribed displacement"""
+
+ # opposed to normal
+ nor = normal(domain, s)
+
+ # vertical sides only
+ clamped = wp.abs(nor[0])
+
+ return -displacement * wp.dot(nor, v(s)) * clamped
+
+
+class ClassicFEMWithFloorAndBC(ClassicFEMWithDensity):
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ ClassicFEM.add_parser_arguments(parser)
+ CollisionHandler.add_parser_arguments(parser)
+
+ def compute_initial_guess(self):
+ self.du_field.dof_values.zero_()
+ self.collision_handler.detect_collisions(self.dt)
+
+ def evaluate_energy(self):
+ E_p, c_r = super().evaluate_energy()
+ E_p = self.collision_handler.add_collision_energy(E_p)
+
+ return E_p, c_r
+
+ def newton_lhs(self):
+ lhs = super().newton_lhs()
+ self.collision_handler.add_collision_hessian(lhs)
+ fem.dirichlet.project_system_matrix(lhs, self.v_bd_matrix)
+
+ return lhs
+
+ def newton_rhs(self, tape=None):
+ rhs = super().newton_rhs(tape)
+ self.collision_handler.add_collision_forces(rhs)
+ self._filter_forces(rhs, tape=tape)
+ return rhs
+
+ def prepare_newton_step(self, tape=None):
+ self.collision_handler.prepare_newton_step(self.dt)
+
+ return super().prepare_newton_step(tape)
+
+ def init_collision_detector(
+ self,
+ vtx_quadrature: fem.PicQuadrature,
+ ):
+ self.collision_handler = CollisionHandler(
+ [], vtx_quadrature.cell_indices, vtx_quadrature.particle_coords, self
+ )
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+
+ class_parser = argparse.ArgumentParser()
+ class_parser.add_argument(
+ "--variant", "-v", choices=["mfem", "classic", "trusty"], default="classic"
+ )
+ class_args, remaining_args = class_parser.parse_known_args()
+
+ if class_args.variant == "mfem":
+ sim_class = MFEM_RS_F
+ elif class_args.variant == "trusty":
+ sim_class = MFEM_sF_S
+ else:
+ sim_class = ClassicFEMWithFloorAndBC
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--mesh", type=str, required=True, help="Path to .msh file")
+ parser.add_argument(
+ "--materials",
+ type=str,
+ default=None,
+ help="Path to .npz material file (optional)",
+ )
+ parser.add_argument(
+ "--k-neighbors",
+ type=int,
+ default=1,
+ help="Number of neighbors for material interpolation",
+ )
+ parser.add_argument(
+ "--resolution",
+ type=int,
+ default=20,
+ help="Resolution for collision radius calculation",
+ )
+ parser.add_argument("--displacement", type=float, default=0.0)
+ parser.add_argument("--grid", action=argparse.BooleanOptionalAction)
+ parser.add_argument("--clamping", type=str, default="right")
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+ parser.add_argument(
+ "--normalize",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Center and scale mesh to a consistent size.",
+ )
+ parser.add_argument(
+ "--normalize-size",
+ type=float,
+ default=1.0,
+ help="Target size for the largest mesh dimension after normalization.",
+ )
+
+ sim_class.add_parser_arguments(parser)
+
+ args = parser.parse_args(remaining_args)
+ args.ground_height = -1.0
+ args.collision_radius = 0.5 / args.resolution
+ args.up_axis = 1
+ args.young_modulus = 10000.0
+ args.density = 500.0
+ args.poisson_ratio = 0.45
+
+ # Load tetmesh from file
+ print(f"Loading mesh from: {args.mesh}")
+ msh = meshio.read(args.mesh, file_format="gmsh")
+ points_np = msh.points.astype(np.float32)
+ if args.normalize:
+ bbox_min = points_np.min(axis=0)
+ bbox_max = points_np.max(axis=0)
+ center = 0.5 * (bbox_min + bbox_max)
+ max_extent = float(np.max(bbox_max - bbox_min))
+ scale = (args.normalize_size / max_extent) if max_extent > 1e-12 else 1.0
+ points_np = (points_np - center) * scale
+ print(f"Normalized mesh: center ~ 0, max dimension -> {args.normalize_size}")
+ pos = wp.array(points_np, dtype=wp.vec3f)
+ assert (
+ msh.cells[0].type == "tetra"
+ ), f"Expected tetra cells, got {msh.cells[0].type}"
+ tets = wp.array(msh.cells[0].data, dtype=wp.int32)
+ pos.requires_grad = True
+ geo = fem.Tetmesh(positions=pos, tet_vertex_indices=tets, build_bvh=True)
+
+ vtx_quadrature = fem.PicQuadrature(fem.Cells(geo), pos)
+
+ print(f"Mesh loaded: {pos.shape[0]} vertices, {tets.shape[0]} tetrahedra")
+
+ # identify cells with > 0 material fraction
+ fraction_space = fem.make_polynomial_space(geo, dtype=float, degree=0)
+ fraction_test = fem.make_test(fraction_space)
+ fraction = fem.integrate(material_fraction_form, fields={"phi": fraction_test})
+ active_cells = wp.array(dtype=int, shape=fraction.shape)
+ wp.launch(mark_active, dim=fraction.shape, inputs=[fraction, active_cells])
+
+ sim = sim_class(geo, active_cells, args)
+ sim.init_displacement_space(None)
+ sim.init_strain_spaces()
+ sim.init_collision_detector(vtx_quadrature)
+
+ if args.materials:
+ print(f"\n{'='*60}")
+ print("Applying spatially varying material properties...")
+ print(f"{'='*60}")
+ material_stats = apply_spatially_varying_materials(
+ sim, args.materials, k_neighbors=args.k_neighbors
+ )
+
+ # spatially varying density at velocity nodes (correct DOF locations)
+ voxel_coords, voxel_materials = load_material_data(args.materials)
+ upsampler = MaterialUpsampler(voxel_coords, voxel_materials)
+ vel_node_pos = sim.u_field.space.node_positions().numpy()
+ mats_u, _ = upsampler.interpolate(vel_node_pos, k=args.k_neighbors)
+ density_u = mats_u[:, 2]
+ if hasattr(sim, "set_density_from_array"):
+ sim.set_density_from_array(density_u)
+ print(
+ f"Assigned spatial density to {density_u.shape[0]} velocity nodes:"
+ f" min={density_u.min():.2f}, max={density_u.max():.2f}"
+ )
+ else:
+ print(f"\nUsing uniform material properties:")
+ print(f" Young's modulus: {args.young_modulus}")
+ print(f" Poisson's ratio: {args.poisson_ratio}")
+ print(f" Density: {args.density}")
+
+ if args.clamping == "sides":
+ boundary_projector_form = clamped_sides
+ elif args.clamping == "edge":
+ boundary_projector_form = clamped_edge
+ else:
+ boundary_projector_form = clamped_right
+
+ sim.set_boundary_condition(
+ boundary_projector_form=boundary_projector_form,
+ boundary_displacement_form=boundary_displacement_form,
+ boundary_displacement_args={
+ "displacement": args.displacement / max(1, args.n_frames)
+ },
+ )
+
+ run_softbody_sim(sim, ui=args.ui)
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/__init__.py b/deps/vomp/simulation/warp.fem/fem_examples/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/__init__.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/collisions.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/collisions.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ee39d90db389171d61de9df4e47c5b2b3b6ded7
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/collisions.py
@@ -0,0 +1,838 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List, Any
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+from warp.sim.collide import triangle_closest_point, TRI_CONTACT_FEATURE_FACE_INTERIOR
+
+import argparse
+
+from fem_examples.mfem.softbody_sim import SoftbodySim
+
+
+class CollisionHandler:
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ parser.add_argument(
+ "--collision_stiffness",
+ "-ck",
+ type=float,
+ default=1.0,
+ help="Multiplier for collision force/energy",
+ )
+ parser.add_argument(
+ "--collision_radius",
+ "-cr",
+ type=float,
+ default=0.1,
+ help="Radius of interaction for collision particles",
+ )
+ parser.add_argument(
+ "--collision_detection_ratio",
+ "-cd",
+ type=float,
+ default=2.0,
+ help="Multiplier of collision radius for detection",
+ )
+ parser.add_argument("--friction", "-mu", type=float, default=0.2)
+ parser.add_argument(
+ "--friction_reg",
+ "-mur",
+ type=float,
+ default=0.1,
+ help="Regularization coefficient for friction",
+ )
+ parser.add_argument(
+ "--friction_fluid",
+ "-nu",
+ type=float,
+ default=0.01,
+ help="Additional fluid friction ratio to convexify things",
+ )
+ parser.add_argument(
+ "--ground",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Do ground collisions",
+ )
+ parser.add_argument(
+ "--ground_height",
+ type=float,
+ default=0.0,
+ help="Ground height",
+ )
+
+ def __init__(
+ self,
+ kinematic_meshes: List[wp.Mesh],
+ cp_cell_indices,
+ cp_cell_coords,
+ sim: SoftbodySim,
+ ):
+ self.args = sim.args
+ self.sim = sim
+ self.warp_meshes = kinematic_meshes
+
+ n_cp = cp_cell_indices.shape[0]
+ collision_quadrature = fem.PicQuadrature(
+ domain=sim.vel_quadrature.domain,
+ positions=(cp_cell_indices, cp_cell_coords),
+ measures=wp.ones(n_cp, dtype=float),
+ )
+ self.set_collision_quadrature(collision_quadrature)
+
+ self.n_contact = 0
+
+ max_contacts = 10 * cp_cell_indices.shape[0]
+ self.collision_indices_a = wp.empty(max_contacts, dtype=int)
+ self.collision_indices_b = wp.empty(max_contacts, dtype=int)
+ self.collision_normals = wp.empty(max_contacts, dtype=wp.vec3)
+ self.collision_kinematic_gaps = wp.empty(max_contacts, dtype=wp.vec3)
+
+ jac_cols = sim.u_field.space_partition.node_count()
+ self._collision_jacobian_a = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+ self._collision_jacobian_b = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+
+ self._collision_jacobian = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+ self._collision_jacobian_t = sp.bsr_zeros(jac_cols, 0, block_type=wp.mat33)
+
+ self._HtH_work_arrays = sp.bsr_mm_work_arrays()
+ self._HbHa_work_arrays = sp.bsr_axpy_work_arrays()
+
+ self._collision_stiffness = (
+ self.args.collision_stiffness * self.args.density / n_cp
+ )
+
+ def set_collision_quadrature(self, quadrature: fem.PicQuadrature):
+ self.collision_quadrature = quadrature
+
+ def add_collision_energy(self, E: float):
+ if self.n_contact == 0:
+ return E
+
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+ col_energies = wp.empty(self.n_contact, dtype=float)
+ wp.launch(
+ collision_energy,
+ dim=self.n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.friction,
+ self.args.dt * self.args.friction_reg,
+ self.args.friction_fluid * self.args.friction_reg,
+ cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ col_energies,
+ ],
+ )
+ return E + self._collision_stiffness * wp.utils.array_sum(col_energies)
+
+ def add_collision_hessian(self, lhs: wp.array):
+ # contacts
+ if self.n_contact == 0:
+ return lhs
+
+ H = self._collision_jacobian
+ Ht = self._collision_jacobian_t
+ wp.launch(
+ bsr_mul_diag,
+ dim=(Ht.nnz_sync(), Ht.block_shape[0]),
+ inputs=[Ht.scalar_values, Ht.columns, self._col_energy_hessian],
+ )
+
+ sp.bsr_mm(
+ x=Ht,
+ y=H,
+ z=lhs,
+ alpha=self._collision_stiffness,
+ beta=1.0,
+ work_arrays=self._HtH_work_arrays,
+ )
+
+ return lhs
+
+ def add_collision_forces(self, rhs: wp.array):
+ if self.n_contact == 0:
+ return rhs
+
+ # contacts
+ sp.bsr_mv(
+ A=self._collision_jacobian_t,
+ x=self._col_energy_gradients,
+ y=rhs,
+ alpha=-self._collision_stiffness,
+ beta=1.0,
+ )
+
+ return rhs
+
+ def prepare_newton_step(self, dt: float):
+ self.detect_collisions(dt)
+ self.build_collision_jacobian()
+
+ # compute per-contact forces and hessian
+ n_contact = self.n_contact
+ if n_contact > 0:
+ self._col_energy_gradients = wp.empty(n_contact, dtype=wp.vec3)
+ self._col_energy_hessian = wp.empty(n_contact, dtype=wp.mat33)
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+
+ wp.launch(
+ collision_gradient_and_hessian,
+ dim=n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.friction,
+ dt * self.args.friction_reg,
+ self.args.friction_fluid * self.args.friction_reg,
+ cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ self._col_energy_gradients,
+ self._col_energy_hessian,
+ ],
+ )
+
+ def cp_world_position(self, dest=None):
+ cp_pic = self.collision_quadrature
+ if dest is None:
+ dest = wp.empty(cp_pic.total_point_count(), dtype=wp.vec3)
+ fem.interpolate(
+ world_position,
+ fields={"u": self.sim.u_field},
+ dest=dest,
+ quadrature=cp_pic,
+ )
+
+ return dest
+
+ def _sample_cp_displacement(self, du_field, dest=None):
+ cp_pic = self.collision_quadrature
+ if dest is None:
+ dest = wp.empty(cp_pic.total_point_count(), dtype=wp.vec3)
+ fem.interpolate(
+ du_field,
+ dest=dest,
+ quadrature=cp_pic,
+ )
+ return dest
+
+ def detect_collisions(self, dt):
+ max_contacts = self.collision_normals.shape[0]
+
+ count = wp.zeros(1, dtype=int)
+ indices_a = self.collision_indices_a
+ indices_b = self.collision_indices_b
+ normals = self.collision_normals
+ kinematic_gaps = self.collision_kinematic_gaps
+
+ self.run_collision_detectors(
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ )
+
+ self.n_contact = int(count.numpy()[0])
+
+ if self.n_contact > max_contacts:
+ print("Warning: contact buffer size exceeded, some have bee ignored")
+ self.n_contact = max_contacts
+
+ def run_collision_detectors(
+ self,
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ ):
+ cp_pic = self.collision_quadrature
+ n_cp = cp_pic.total_point_count()
+ max_contacts = self.collision_normals.shape[0]
+
+ cp_cur_pos = self.cp_world_position()
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+
+ collision_radius = (
+ self.args.collision_radius * self.args.collision_detection_ratio
+ )
+
+ if self.args.ground:
+ ground_height = self.args.ground_height
+ wp.launch(
+ detect_ground_collisions,
+ dim=n_cp,
+ inputs=[
+ max_contacts,
+ self.args.up_axis,
+ cp_cur_pos,
+ cp_du,
+ collision_radius,
+ ground_height,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+ if self.warp_meshes:
+ mesh_ids = wp.array([mesh.id for mesh in self.warp_meshes], dtype=wp.uint64)
+ wp.launch(
+ detect_mesh_collisions,
+ dim=(len(mesh_ids), n_cp),
+ inputs=[
+ max_contacts,
+ dt,
+ mesh_ids,
+ cp_cur_pos,
+ cp_du,
+ collision_radius,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+
+ def build_collision_jacobian(self):
+ n_contact = self.n_contact
+
+ # Build collision jacobian
+ # (derivative of collision gap `pos_a - pos_b` w.r.t. degrees of freedom)
+
+ if n_contact == 0:
+ return
+
+ a_cells = wp.empty(n_contact, dtype=int)
+ a_coords = wp.empty(n_contact, dtype=wp.vec3)
+ b_cells = wp.empty(n_contact, dtype=int)
+ b_coords = wp.empty(n_contact, dtype=wp.vec3)
+ wp.launch(
+ gather_cell_coordinates,
+ dim=n_contact,
+ inputs=[
+ self.collision_quadrature.cell_indices,
+ self.collision_quadrature.particle_coords,
+ self.collision_indices_a,
+ a_cells,
+ a_coords,
+ ],
+ )
+ wp.launch(
+ gather_cell_coordinates,
+ dim=n_contact,
+ inputs=[
+ self.collision_quadrature.cell_indices,
+ self.collision_quadrature.particle_coords,
+ self.collision_indices_b,
+ b_cells,
+ b_coords,
+ ],
+ )
+
+ measures = wp.ones(n_contact, dtype=float)
+
+ a_contact_pic = fem.PicQuadrature(
+ self.collision_quadrature.domain,
+ positions=(a_cells, a_coords),
+ measures=measures,
+ )
+ b_contact_pic = fem.PicQuadrature(
+ self.collision_quadrature.domain,
+ positions=(b_cells, b_coords),
+ measures=measures,
+ )
+ u_trial = fem.make_trial(
+ self.sim.u_field.space, space_partition=self.sim.u_field.space_partition
+ )
+
+ sp.bsr_set_zero(
+ self._collision_jacobian_a,
+ n_contact,
+ self.sim.u_field.space_partition.node_count(),
+ )
+ fem.interpolate(
+ u_trial,
+ quadrature=a_contact_pic,
+ dest=self._collision_jacobian_a,
+ bsr_options={"prune_numerical_zeros": False},
+ )
+
+ sp.bsr_set_zero(
+ self._collision_jacobian_b,
+ n_contact,
+ self.sim.u_field.space_partition.node_count(),
+ )
+ fem.interpolate(
+ u_trial,
+ quadrature=b_contact_pic,
+ dest=self._collision_jacobian_b,
+ bsr_options={"prune_numerical_zeros": False},
+ )
+
+ self._collision_jacobian_a.nnz_sync()
+ self._collision_jacobian_b.nnz_sync()
+
+ sp.bsr_assign(self._collision_jacobian, src=self._collision_jacobian_a)
+ sp.bsr_axpy(
+ x=self._collision_jacobian_b,
+ y=self._collision_jacobian,
+ alpha=-1,
+ beta=1,
+ work_arrays=self._HbHa_work_arrays,
+ )
+
+ sp.bsr_set_transpose(
+ dest=self._collision_jacobian_t, src=self._collision_jacobian
+ )
+
+
+class MeshSelfCollisionHandler(CollisionHandler):
+ def __init__(
+ self,
+ vtx_quadrature: fem.PicQuadrature,
+ tri_mesh: wp.Mesh,
+ sim: SoftbodySim,
+ ):
+ super().__init__(
+ [], vtx_quadrature.cell_indices, vtx_quadrature.particle_coords, sim
+ )
+
+ self.tri_vtx_quadrature = vtx_quadrature
+ self.vtx_rest_pos = wp.clone(tri_mesh.points)
+ self.tri_mesh = tri_mesh
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ CollisionHandler.add_parser_arguments(parser)
+ parser.add_argument(
+ "--self_immunity_radius_ratio",
+ "-cs",
+ type=float,
+ default=4.0,
+ help="Ignore self-collision for particles that were within this ratio at rest",
+ )
+
+ def run_collision_detectors(
+ self,
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ ):
+ self.set_collision_quadrature(self.tri_vtx_quadrature)
+
+ super().run_collision_detectors(
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ )
+ self.cp_world_position(dest=self.tri_mesh.points)
+ self.tri_mesh.refit()
+
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+
+ n_cp = cp_du.shape[0]
+ max_contacts = self.collision_normals.shape[0]
+
+ collision_radius = (
+ self.args.collision_radius * self.args.collision_detection_ratio
+ )
+
+ start_contacts = count.numpy()[0]
+ pos_b = wp.empty(indices_b.shape, dtype=wp.vec3)
+
+ wp.launch(
+ detect_mesh_self_collisions,
+ dim=(n_cp),
+ inputs=[
+ start_contacts,
+ max_contacts,
+ dt,
+ self.args.self_immunity_radius_ratio,
+ self.tri_mesh.id,
+ self.vtx_rest_pos,
+ cp_du,
+ collision_radius,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ pos_b,
+ ],
+ )
+ self_contacts = int(min(max_contacts, count.numpy()[0]) - start_contacts)
+
+ if self_contacts > 0:
+ contact_points = wp.empty(n_cp + self_contacts, dtype=wp.vec3)
+ wp.copy(contact_points[:n_cp], self.vtx_rest_pos)
+ wp.copy(contact_points[n_cp:], pos_b[:self_contacts])
+
+ quadrature = fem.PicQuadrature(
+ fem.Cells(self.sim.geo),
+ contact_points,
+ max_dist=self.sim.typical_length,
+ )
+ self.set_collision_quadrature(quadrature)
+
+
+@wp.kernel
+def bsr_mul_diag(
+ Bt_values: wp.array3d(dtype=float),
+ Bt_columns: wp.array(dtype=int),
+ C_values: wp.array(dtype=Any),
+):
+ i, r = wp.tid()
+ col = Bt_columns[i]
+
+ C = C_values[col]
+
+ Btr = Bt_values[i, r]
+ BtC = wp.vec3(Btr[0], Btr[1], Btr[2]) @ C
+ for k in range(3):
+ Btr[k] = BtC[k]
+
+
+@wp.kernel
+def detect_ground_collisions(
+ max_contacts: int,
+ up_axis: int,
+ pos_cur: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ ground_height: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ i = wp.tid()
+ x = pos_cur[i]
+
+ if x[up_axis] < ground_height + radius:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ nor = wp.vec3()
+ nor[up_axis] = 1.0
+
+ normals[idx] = nor
+ kinematic_gaps[idx] = (wp.dot(x - du_cur[i], nor) - ground_height) * nor
+ indices_a[idx] = i
+ indices_b[idx] = fem.NULL_QP_INDEX
+
+
+@wp.kernel
+def detect_mesh_collisions(
+ max_contacts: int,
+ dt: float,
+ mesh_ids: wp.array(dtype=wp.uint64),
+ pos_cur: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ m, tid = wp.tid()
+ mesh_id = mesh_ids[m]
+
+ x = pos_cur[tid]
+
+ query = wp.mesh_query_point(mesh_id, x, radius)
+
+ if query.result:
+ cp = wp.mesh_eval_position(mesh_id, query.face, query.u, query.v)
+
+ delta = x - cp
+ dist = wp.length(delta) * query.sign
+
+ if dist < radius:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ if dist < 0.00001:
+ n = wp.mesh_eval_face_normal(mesh_id, query.face)
+ else:
+ n = wp.normalize(delta) * query.sign
+ normals[idx] = n
+
+ v = wp.mesh_eval_velocity(mesh_id, query.face, query.u, query.v)
+
+ kinematic_gap = (dist - wp.dot(du_cur[tid], n)) * n - v * dt
+ kinematic_gaps[idx] = kinematic_gap
+ indices_a[idx] = tid
+ indices_b[idx] = fem.NULL_QP_INDEX
+
+
+@wp.kernel
+def detect_mesh_self_collisions(
+ cur_contacts: int,
+ max_contacts: int,
+ dt: float,
+ self_immunity_ratio: float,
+ mesh_id: wp.uint64,
+ mesh_rest_pos: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ pos_b: wp.array(dtype=wp.vec3),
+):
+ tid = wp.tid()
+ mesh = wp.mesh_get(mesh_id)
+
+ x = mesh.points[tid]
+
+ lower = x - wp.vec3(radius)
+ upper = x + wp.vec3(radius)
+
+ query = wp.mesh_query_aabb(mesh_id, lower, upper)
+
+ face_index = wp.int32(0)
+ while wp.mesh_query_aabb_next(query, face_index):
+ t0 = mesh.indices[3 * face_index + 0]
+ t1 = mesh.indices[3 * face_index + 1]
+ t2 = mesh.indices[3 * face_index + 2]
+ if tid == t0 or tid == t1 or tid == t2:
+ # Fast self collision
+ continue
+
+ u1 = mesh.points[t0]
+ u2 = mesh.points[t1]
+ u3 = mesh.points[t2]
+
+ cp, bary, feature_type = triangle_closest_point(u1, u2, u3, x)
+ if feature_type != TRI_CONTACT_FEATURE_FACE_INTERIOR:
+ continue
+
+ delta = x - cp
+
+ face_nor = wp.mesh_eval_face_normal(mesh_id, face_index)
+ sign = wp.where(wp.dot(delta, face_nor) > 0.0, 1.0, -1.0)
+
+ dist = wp.length(delta) * sign
+
+ if dist < radius:
+ # discard self-collisions of points that were very close at rest
+ rp0 = mesh_rest_pos[t0]
+ rp1 = mesh_rest_pos[t1]
+ rp2 = mesh_rest_pos[t2]
+ xb_rest = bary[0] * rp0 + bary[1] * rp1 + bary[2] * rp2
+ xa_rest = mesh_rest_pos[tid]
+ if wp.length(xb_rest - xa_rest) < self_immunity_ratio * radius:
+ continue
+
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ if dist < 0.00001:
+ n = face_nor
+ else:
+ n = wp.normalize(delta) * sign
+ normals[idx] = n
+
+ du0 = du_cur[t0]
+ du1 = du_cur[t1]
+ du2 = du_cur[t2]
+ du = du_cur[tid] - du0 * bary[0] - du1 * bary[1] - du2 * bary[2]
+
+ kinematic_gap = (dist - wp.dot(du, n)) * n
+ kinematic_gaps[idx] = kinematic_gap
+ indices_a[idx] = tid
+ indices_b[idx] = mesh.points.shape[0] + idx - cur_contacts
+ pos_b[idx - cur_contacts] = xb_rest
+
+
+@wp.func
+def collision_offset(
+ c: int,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ idx_a = indices_a[c]
+ idx_b = indices_b[c]
+
+ offset = du_cur[idx_a] + kinematic_gaps[c]
+ if idx_b != fem.NULL_QP_INDEX:
+ offset -= du_cur[idx_b]
+
+ return offset
+
+
+@wp.func
+def collision_target_distance(
+ c: int,
+ radius: float,
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ return wp.where(indices_b[c] == fem.NULL_ELEMENT_INDEX, 1.0, 2.0) * radius
+
+
+@wp.kernel
+def collision_energy(
+ radius: float,
+ mu: float,
+ dt: float,
+ nu: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ energies: wp.array(dtype=float),
+):
+ c = wp.tid()
+
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+
+ nor = normals[c]
+ d = wp.dot(offset, nor)
+ d_hat = d / rc
+
+ stick = wp.where(d_hat < 1.0, 1.0, 0.0)
+ gap = d_hat - 1.0
+ E = 0.5 * stick * gap * gap
+
+ vt = (offset - d * nor) / dt # tangential velocity
+ vt_norm = wp.length(vt)
+
+ mu_fn = -mu * wp.min(0.0, gap) / rc # yield force
+
+ E += (
+ mu_fn
+ * dt
+ * (
+ 0.5 * nu * vt_norm * vt_norm
+ + wp.where(
+ vt_norm < 1.0,
+ vt_norm * vt_norm * (1.0 - vt_norm / 3.0),
+ vt_norm - 1.0 / 3.0,
+ )
+ )
+ )
+
+ energies[c] = E
+
+
+@wp.kernel
+def collision_gradient_and_hessian(
+ radius: float,
+ mu: float,
+ dt: float,
+ nu: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ gradient: wp.array(dtype=wp.vec3),
+ hessian: wp.array(dtype=wp.mat33),
+):
+ c = wp.tid()
+
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+
+ nor = normals[c]
+ d = wp.dot(offset, nor)
+ d_hat = d / rc
+
+ stick = wp.where(d_hat < 1.0, 1.0, 0.0)
+
+ dE_d_hat = d_hat - 1.0
+ gradient[c] = dE_d_hat * stick / rc * nor
+ hessian[c] = wp.outer(nor, nor) * stick / (rc * rc)
+
+ vt = (offset - d * nor) / dt # tangential velocity
+ vt_norm = wp.length(vt)
+ vt_dir = wp.normalize(vt) # avoids dealing with 0
+
+ mu_fn = -mu * wp.min(0.0, dE_d_hat) / rc # yield force
+
+ f1_over_vt_norm = wp.where(vt_norm < 1.0, 2.0 - vt_norm, 1.0 / vt_norm)
+ gradient[c] += mu_fn * (f1_over_vt_norm + nu) * vt
+
+ # regularization such that f / H dt <= k v (penalizes friction switching dir)
+ friction_slip_reg = 0.1
+ df1_d_vtn = wp.max(
+ 2.0 * (1.0 - vt_norm),
+ friction_slip_reg / (0.5 * friction_slip_reg + vt_norm),
+ )
+
+ vt_perp = wp.cross(vt_dir, nor)
+ hessian[c] += (
+ mu_fn
+ / dt
+ * (
+ (df1_d_vtn + nu) * wp.outer(vt_dir, vt_dir)
+ + (f1_over_vt_norm + nu) * wp.outer(vt_perp, vt_perp)
+ )
+ )
+
+
+@wp.kernel
+def gather_cell_coordinates(
+ qp_cells: wp.array(dtype=int),
+ qp_coords: wp.array(dtype=wp.vec3),
+ indices: wp.array(dtype=int),
+ cells: wp.array(dtype=int),
+ coords: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+ qp = indices[i]
+
+ if qp == fem.NULL_QP_INDEX:
+ cells[i] = fem.NULL_ELEMENT_INDEX
+ else:
+ cells[i] = qp_cells[qp]
+ coords[i] = qp_coords[qp]
+
+
+@fem.integrand
+def world_position(s: fem.Sample, domain: fem.Domain, u: fem.Field):
+ return domain(s) + u(s)
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/demo_3d.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/demo_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..3567e0aefd1bd1ed7ff6f248b100cd1e7145b31d
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/demo_3d.py
@@ -0,0 +1,221 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Domain, Sample, Field
+from warp.fem import integrand, normal
+
+from .softbody_sim import ClassicFEM, run_softbody_sim
+from .mfem_3d import MFEM_RS_F, MFEM_sF_S
+
+import warp.examples.fem.utils as fem_example_utils
+
+# Demo ap
+
+
+@wp.func
+def material_fraction(x: wp.vec3):
+ # arbitrary notch in the grid
+ return 1.0
+ # return wp.select(wp.length(x - wp.vec3(0.5, 1.0, 0.875)) > 0.2, 0.0, 1.0)
+
+
+@integrand
+def material_fraction_form(s: Sample, domain: Domain, phi: Field):
+ return material_fraction(domain(s)) * phi(s)
+
+
+@wp.kernel
+def mark_active(fraction: wp.array(dtype=wp.float64), active: wp.array(dtype=int)):
+ active[wp.tid()] = int(wp.nonzero(fraction[wp.tid()]))
+
+
+@integrand
+def clamped_edge(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ clamped = float(0.0)
+
+ # Single clamped edge
+ if s.qp_index < 10:
+ clamped = 1.0
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamped_right(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ pos = domain(s)
+ clamped = float(0.0)
+
+ # clamped right sides
+ # clamped = wp.where(pos[0] < 1.0, 0.0, 1.0)
+ clamped = wp.where(pos[0] < 0.97, 0.0, 1.0)
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamped_sides(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = normal(domain, s)
+ clamped = float(0.0)
+
+ # clamped vertical sides
+ clamped = wp.abs(nor[0])
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def boundary_displacement_form(
+ s: Sample,
+ domain: Domain,
+ v: Field,
+ displacement: wp.vec3,
+):
+ """Prescribed displacement"""
+
+ # # opposed to normal
+ # # nor = normal(domain, s)
+ # # nor = wp.vec3(1.0, 0.0, 0.0)
+ # nor = wp.vec3(1.0, 1.0, 1.0)
+ # nor = nor / wp.length(nor)
+
+ # # vertical sides only
+ # clamped = wp.abs(nor[0])
+ # print(displacement)
+
+ # return -displacement * wp.dot(nor, v(s)) * clamped
+ return wp.dot(displacement, v(s))
+
+
+# @integrand
+# def boundary_displacement_form(
+# s: Sample,
+# domain: Domain,
+# v: Field,
+# displacement: float,
+# ):
+# """Prescribed displacement"""
+
+# # opposed to normal
+# # nor = normal(domain, s)
+# # nor = wp.vec3(1.0, 0.0, 0.0)
+# nor = wp.vec3(1.0, 1.0, 1.0)
+# nor = nor / wp.length(nor)
+
+# # vertical sides only
+# clamped = wp.abs(nor[0])
+# print(displacement)
+
+# # return -displacement * wp.dot(nor, v(s)) * clamped
+# return displacement * wp.dot(nor, v(s)) * clamped
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+
+ class_parser = argparse.ArgumentParser()
+ class_parser.add_argument(
+ "--variant", "-v", choices=["mfem", "classic", "trusty"], default="mfem"
+ )
+ class_args, remaining_args = class_parser.parse_known_args()
+
+ if class_args.variant == "mfem":
+ sim_class = MFEM_RS_F
+ elif class_args.variant == "trusty":
+ sim_class = MFEM_sF_S
+ else:
+ sim_class = ClassicFEM
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument("--displacement", type=float, default=0.0)
+ parser.add_argument("--grid", action=argparse.BooleanOptionalAction)
+ parser.add_argument("--clamping", type=str, default="right")
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+ sim_class.add_parser_arguments(parser)
+ args = parser.parse_args(remaining_args)
+
+ if args.grid:
+ geo = fem.Grid3D(
+ res=wp.vec3i(args.resolution), bounds_lo=wp.vec3(0.0, 0.75, 0.75)
+ )
+ else:
+ pos, tets = fem_example_utils.gen_tetmesh(
+ res=wp.vec3i(args.resolution), bounds_lo=wp.vec3(0.0, 0.75, 0.75)
+ )
+ pos.requires_grad = True
+ geo = fem.Tetmesh(positions=pos, tet_vertex_indices=tets)
+
+ # identify cells with > 0 material fraction
+ fraction_space = fem.make_polynomial_space(geo, dtype=float, degree=0)
+ fraction_test = fem.make_test(fraction_space)
+ fraction = fem.integrate(material_fraction_form, fields={"phi": fraction_test})
+ active_cells = wp.array(dtype=int, shape=fraction.shape)
+ wp.launch(mark_active, dim=fraction.shape, inputs=[fraction, active_cells])
+
+ sim = sim_class(geo, active_cells, args)
+ sim.init_displacement_space(None)
+ sim.init_strain_spaces()
+
+ if args.clamping == "sides":
+ boundary_projector_form = clamped_sides
+ elif args.clamping == "edge":
+ boundary_projector_form = clamped_edge
+ else:
+ import pdb
+
+ pdb.set_trace()
+ boundary_projector_form = clamped_right
+
+ sim.set_boundary_condition(
+ boundary_projector_form=boundary_projector_form,
+ boundary_displacement_form=boundary_displacement_form,
+ boundary_displacement_args={
+ # "displacement": args.displacement / max(1, args.n_frames)
+ "displacement": wp.vec3(
+ 0.0 / max(1, args.n_frames),
+ 1.0 / max(1, args.n_frames),
+ 1.0 / max(1, args.n_frames),
+ ),
+ },
+ )
+
+ run_softbody_sim(sim, ui=args.ui)
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/elastic_models.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/elastic_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..dffb753836b125747fd86118f8c567a97dc6fd46
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/elastic_models.py
@@ -0,0 +1,365 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import warp as wp
+import math
+
+__all__ = [
+ "hooke_energy",
+ "hooke_stress",
+ "hooke_hessian",
+ "nh_energy",
+ "nh_stress",
+ "nh_hessian_proj",
+ "nh_hessian_proj_analytic",
+ "snh_energy",
+ "snh_stress",
+ "snh_hessian_proj",
+ "snh_hessian_proj_analytic",
+]
+
+_SQRT_1_2 = wp.constant(math.sqrt(1.0 / 2.0))
+
+
+@wp.func
+def hooke_stress(S: wp.mat33, lame: wp.vec2):
+ strain = S - wp.identity(n=3, dtype=float)
+ return 2.0 * lame[1] * strain + lame[0] * wp.trace(strain) * wp.identity(
+ n=3, dtype=float
+ )
+
+
+@wp.func
+def hooke_energy(S: wp.mat33, lame: wp.vec2):
+ strain = S - wp.identity(n=3, dtype=float)
+ return 0.5 * wp.ddot(strain, hooke_stress(S, lame))
+
+
+@wp.func
+def hooke_hessian(S: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ return wp.ddot(hooke_stress(sig + wp.identity(n=3, dtype=float), lame), tau)
+
+
+# Neo-Hookean -- Eq (13) from Smith et al paper
+#
+
+
+@wp.func
+def nh_parameters_from_lame(lame: wp.vec2):
+ """Parameters such that for small strains model behaves according to Hooke's law"""
+ mu_nh = lame[1]
+ lambda_nh = lame[0] + lame[1]
+
+ return mu_nh, lambda_nh
+
+
+@wp.func
+def nh_energy(F: wp.mat33, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ E0 = lambda_nh * (1.0 - gamma) * (1.0 - gamma) + mu_nh * 3.0
+
+ return 0.5 * (lambda_nh * (J - gamma) * (J - gamma) + mu_nh * wp.ddot(F, F) - E0)
+
+
+@wp.func
+def nh_stress(F: wp.mat33, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ return mu_nh * F + lambda_nh * (J - gamma) * _dJ_dF(F)
+
+
+@wp.func
+def nh_hessian_proj(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ dJ_dF_s = _dJ_dF(F)
+
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ dpsi_dpsi = mu_nh * wp.ddot(tau, sig) + lambda_nh * wp.ddot(dJ_dF_s, tau) * wp.ddot(
+ dJ_dF_s, sig
+ )
+
+ # clamp the hessian of J so that it does not compritube eigenvalue smaller than - mu * F_scale
+ J = wp.determinant(F)
+ Ic = wp.ddot(F, F)
+ gamma = 1.0 + mu_nh / lambda_nh
+ muT = mu_nh
+ d2J_scale = _d2J_dF2_scale(J, Ic, lambda_nh * (J - gamma), 0.99 * muT)
+ # d2J_scale = lambda_nh * (J - gamma)
+ # d2J_scale = 0.0
+
+ return dpsi_dpsi + d2J_scale * _d2J_dF2(F, sig, tau)
+
+
+@wp.func
+def nh_hessian_proj_analytic(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+
+ J = wp.determinant(F)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ muT = mu_nh
+ muL = 0.0
+ lbdJ = lambda_nh * (J - gamma)
+
+ return hessian_proj_analytic(F, muT, muL, lambda_nh, lbdJ, sig, tau)
+
+
+# Stable Neo-Hookean -- complete model from Simat et al. with log(Ic + 1) term
+#
+
+
+@wp.func
+def snh_parameters_from_lame(lame: wp.vec2):
+ """Parameters such that for small strains model behaves according to Hooke's law"""
+ mu_nh = 4.0 / 3.0 * lame[1]
+ lambda_nh = lame[0] + 5.0 / 6.0 * lame[1]
+
+ return mu_nh, lambda_nh
+
+
+@wp.func
+def snh_energy(F: wp.mat33, lame: wp.vec2):
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ J = wp.determinant(F)
+ Ic = wp.ddot(F, F)
+
+ E0 = lambda_nh * (1.0 - gamma) * (1.0 - gamma) + mu_nh * (3.0 - wp.log(4.0))
+
+ return 0.5 * (
+ lambda_nh * (J - gamma) * (J - gamma) + mu_nh * (Ic - wp.log(Ic + 1.0)) - E0
+ )
+
+
+@wp.func
+def snh_stress(F: wp.mat33, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ Ic = wp.ddot(F, F)
+ F_scale = 1.0 - 1.0 / (Ic + 1.0)
+ return mu_nh * F * F_scale + lambda_nh * (J - gamma) * _dJ_dF(F)
+
+
+@wp.func
+def snh_hessian_proj(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ dJ_dF_s = _dJ_dF(F)
+
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+
+ Ic = wp.ddot(F, F)
+
+ F_scale = 1.0 - 1.0 / (Ic + 1.0)
+
+ dpsi_dpsi = (
+ mu_nh * F_scale * wp.ddot(tau, sig)
+ + 2.0 * mu_nh / ((Ic + 1.0) * (Ic + 1.0)) * wp.ddot(F, tau) * wp.ddot(F, sig)
+ + lambda_nh * wp.ddot(dJ_dF_s, tau) * wp.ddot(dJ_dF_s, sig)
+ )
+
+ # clamp the hessian of J so that it does not compritube eigenvalue smaller than - mu * F_scale
+ J = wp.determinant(F)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+ muT = mu_nh * F_scale
+ d2J_scale = _d2J_dF2_scale(J, Ic, lambda_nh * (J - gamma), 0.99 * muT)
+ # d2J_scale = lambda_nh * (J - gamma)
+ # d2J_scale = 0.0
+
+ return dpsi_dpsi + d2J_scale * _d2J_dF2(F, sig, tau)
+
+
+@wp.func
+def snh_hessian_proj_analytic(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+
+ Ic = wp.ddot(F, F)
+ J = wp.determinant(F)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ muT = mu_nh * (1.0 - 1.0 / (Ic + 1.0))
+ muL = 2.0 * mu_nh / ((Ic + 1.0) * (Ic + 1.0))
+ lbdJ = lambda_nh * (J - gamma)
+
+ return hessian_proj_analytic(F, muT, muL, lambda_nh, lbdJ, sig, tau)
+
+
+# Utilities
+
+
+@wp.func
+def hessian_proj_analytic(
+ F: wp.mat33,
+ muT: float,
+ muL: float,
+ lbd: float,
+ lbdJ: float,
+ sig: wp.mat33,
+ tau: wp.mat33,
+):
+ U = wp.mat33()
+ S = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, S, V)
+
+ # Solve eigensystem on principal stresses -- analytical is ugly
+ # (and formula (44) for eigenvectors does not seem to yield the correct result)
+ Scross = wp.vec3(S[1] * S[2], S[0] * S[2], S[0] * S[1])
+ Soff = wp.mat33(0.0, S[2], S[1], S[2], 0.0, S[0], S[1], S[0], 0.0)
+ A = (
+ muT * wp.identity(n=3, dtype=float)
+ + muL * wp.outer(S, S)
+ + lbd * wp.outer(Scross, Scross)
+ + lbdJ * Soff
+ )
+
+ Q = wp.mat33()
+ d = wp.vec3()
+ wp.eig3(A, Q, d)
+ Qt = wp.transpose(Q)
+
+ # d, Qt = fem.utils.symmetric_eigenvalues_qr(A, 1.0e-16)
+
+ # accumulate eigenvectors corresponding to positive eigenvalues
+ tauU = wp.transpose(U) * tau
+ sigU = wp.transpose(U) * sig
+ Vt = wp.transpose(V)
+
+ res = float(0.0)
+ clamp = 0.0
+
+ for k in range(3):
+ Pk = wp.diag(Qt[k]) * Vt
+ res += wp.max(clamp, d[k]) * wp.ddot(tauU, Pk) * wp.ddot(sigU, Pk)
+
+ Pk = _flip_rot_eivec(k, 1.0, Vt)
+ res += wp.max(clamp, muT + lbdJ * S[k]) * wp.ddot(tauU, Pk) * wp.ddot(sigU, Pk)
+
+ Pk = _flip_rot_eivec(k, -1.0, Vt)
+ res += wp.max(clamp, muT - lbdJ * S[k]) * wp.ddot(tauU, Pk) * wp.ddot(sigU, Pk)
+
+ return res
+
+
+@wp.func
+def _flip_rot_eivec(k: int, sign: float, mat: wp.mat33):
+ E = wp.mat33(0.0)
+ k2 = (k + 2) % 3
+ k1 = (k + 1) % 3
+ E[k1] = _SQRT_1_2 * mat[k2]
+ E[k2] = -sign * _SQRT_1_2 * mat[k1]
+ return E
+
+
+@wp.func
+def _dJ_dF(F: wp.mat33):
+ Ft = wp.transpose(F)
+ return wp.mat33(
+ wp.cross(Ft[1], Ft[2]), wp.cross(Ft[2], Ft[0]), wp.cross(Ft[0], Ft[1])
+ )
+
+
+@wp.func
+def _d2J_dF2(F: wp.mat33, sig: wp.mat33, tau: wp.mat33):
+ Ft = wp.transpose(F)
+ sigt = wp.transpose(sig)
+ return wp.ddot(
+ tau,
+ wp.mat33(
+ wp.cross(Ft[1], sigt[2]) + wp.cross(sigt[1], Ft[2]),
+ wp.cross(Ft[2], sigt[0]) + wp.cross(sigt[2], Ft[0]),
+ wp.cross(Ft[0], sigt[1]) + wp.cross(sigt[0], Ft[1]),
+ ),
+ )
+
+
+@wp.func
+def _d2J_dF2_scale(J: float, Ic: float, J_scale: float, Id_scale: float):
+ # compute a scaling for d2J such that Id_scale * Id + J_scale * d2J
+ # has no negative eigenvalues
+
+ # Min/max eigenvalues for d2J are estimated according to
+ # sec 4.5 of "Stable Neo-Hookean Flesh Simulation" (Smith et al. 2018)
+
+ d2J_ev = _depressed_cubic_roots(-Ic, -2.0 * J)
+ sig_max = wp.sqrt(Ic)
+
+ ev_min = wp.min(wp.min(d2J_ev), -sig_max)
+ ev_max = wp.max(wp.max(d2J_ev), sig_max)
+ return wp.clamp(J_scale, -Id_scale / ev_max, -Id_scale / ev_min)
+
+
+@wp.func
+def _depressed_cubic_roots(p: float, q: float):
+ alpha = wp.sqrt(-p / 3.0)
+ beta = wp.acos(1.5 * q / (p * alpha)) / 3.0
+ return (
+ 2.0
+ * alpha
+ * wp.vec3(
+ wp.cos(beta),
+ wp.cos(beta - 2.0 / 3.0 * wp.pi),
+ wp.cos(beta - 4.0 / 3.0 * wp.pi),
+ )
+ )
+
+
+@wp.func
+def symmetric_strain(sig: wp.vec3, V: wp.mat33):
+ return V * wp.diag(sig) * wp.transpose(V)
+
+
+@wp.func
+def symmetric_strain(F: wp.mat33):
+ U = wp.mat33()
+ sig = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, sig, V)
+
+ return symmetric_strain(sig, V)
+
+
+@wp.func
+def symmetric_strain_delta(U: wp.mat33, sig: wp.vec3, V: wp.mat33, dF: wp.mat33):
+ # see supplementary of `WRAPD: Weighted Rotation-aware ADMM`, Brown and Narain 21
+
+ Ut = wp.transpose(U)
+ Vt = wp.transpose(V)
+
+ dF_loc = Ut * dF * V
+ SigdF_loc = wp.diag(sig) * dF_loc
+
+ sig_op = wp.matrix_from_cols(wp.vec3(sig[0]), wp.vec3(sig[1]), wp.vec3(sig[2]))
+ dSig = wp.cw_div(SigdF_loc + wp.transpose(SigdF_loc), sig_op + wp.transpose(sig_op))
+ dS = V * dSig * Vt
+
+ return dS
+
+
+@wp.func
+def symmetric_strain_delta(F: wp.mat33, dF: wp.mat33):
+ # see supplementary of `WRAPD: Weighted Rotation-aware ADMM`, Brown and Narain 21
+
+ U = wp.mat33()
+ sig = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, sig, V)
+
+ return symmetric_strain_delta(U, sig, V, dF)
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/linalg.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..71586701c7498643f2244b1ef7ba91ee1dc2430e
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/linalg.py
@@ -0,0 +1,470 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Any, Tuple
+
+import numpy as np
+import warp as wp
+import warp.sparse as sp
+from warp.fem.utils import inverse_qr
+from warp.fem.utils import array_axpy
+
+from warp.examples.fem.utils import bsr_cg
+
+wp.set_module_options({"enable_backward": False})
+wp.set_module_options({"fast_math": True})
+
+
+def diff_bsr_mv(
+ A: sp.BsrMatrix,
+ x: wp.array,
+ y: wp.array,
+ alpha: float = 1.0,
+ beta: float = 0.0,
+ transpose: bool = False,
+ self_adjoint: bool = False,
+):
+ """Performs y = alpha*A*x + beta*y and records the adjoint on the tape"""
+
+ from warp.context import runtime
+
+ tape = runtime.tape
+ if tape is not None and (x.requires_grad or y.requires_grad):
+
+ def backward():
+ # adj_x += adj_y * alpha
+ # adj_y = adj_y * beta
+
+ sp.bsr_mv(
+ A=A,
+ x=y.grad,
+ y=x.grad,
+ alpha=alpha,
+ beta=1.0,
+ transpose=(not transpose) and (not self_adjoint),
+ )
+ if beta != 1.0:
+ array_axpy(x=y.grad, y=y.grad, alpha=0.0, beta=beta)
+
+ runtime.tape.record_func(backward, arrays=[x, y])
+
+ runtime.tape = None
+ # in case array_axpy eventually records its own stuff
+ sp.bsr_mv(A, x, y, alpha, beta, transpose)
+ runtime.tape = tape
+
+
+# MFEM SYSTEM
+# for F = RS variant
+#
+# [ A -B' ]
+# [ H Cd' ]
+# [ W Cr' ]
+# [ -B Cs Cr 0 ]
+#
+
+
+class MFEMSystem:
+ """Builds a linear operator corresponding to the saddle-point linear system [A B^T; B 0]"""
+
+ def __init__(
+ self,
+ A: sp.BsrMatrix,
+ H: sp.BsrMatrix,
+ W: sp.BsrMatrix,
+ B: sp.BsrMatrix,
+ Cs: sp.BsrMatrix,
+ Cr: sp.BsrMatrix,
+ Bt: sp.BsrMatrix = None,
+ ):
+ self._A = A
+ self._H = H
+ self._W = W
+ self._B = B
+ self._Bt = Bt
+ self._Cs = Cs
+ self._Cr = Cr
+
+ def cast(self, scalar_type):
+ if wp.types.types_equal(scalar_type, self._A.scalar_type):
+ return self
+
+ return MFEMSystem(
+ sp.bsr_copy(self._A, scalar_type=scalar_type),
+ sp.bsr_copy(self._H, scalar_type=scalar_type),
+ (
+ sp.bsr_copy(self._W, scalar_type=scalar_type)
+ if self._W is not None
+ else None
+ ),
+ sp.bsr_copy(self._B, scalar_type=scalar_type),
+ sp.bsr_copy(self._Cs, scalar_type=scalar_type),
+ (
+ sp.bsr_copy(self._Cr, scalar_type=scalar_type)
+ if self._W is not None
+ else None
+ ),
+ )
+
+ def solve_schur(
+ lhs,
+ rhs: Tuple,
+ tol=1.0e-8,
+ max_iters=1000,
+ work_arrays=None,
+ reuse_topology=False,
+ ):
+ rhs_type = wp.types.type_scalar_type(rhs[0].dtype)
+ lhs_type = lhs._A.scalar_type
+
+ if not wp.types.types_equal(lhs_type, rhs_type):
+ rhs_cast = tuple(
+ wp.empty(
+ shape=v.shape,
+ dtype=wp.vec(length=wp.types.type_length(v.dtype), dtype=lhs_type),
+ )
+ for v in rhs
+ )
+ for v, v_cast in zip(rhs, rhs_cast):
+ wp.utils.array_cast(in_array=v, out_array=v_cast)
+
+ res = lhs.solve_schur(
+ rhs_cast,
+ tol=tol,
+ max_iters=max_iters,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+
+ res_cast = tuple(
+ wp.empty(
+ shape=v.shape,
+ dtype=wp.vec(length=wp.types.type_length(v.dtype), dtype=rhs_type),
+ )
+ for v in res
+ )
+ for v, v_cast in zip(res, res_cast):
+ wp.utils.array_cast(in_array=v, out_array=v_cast)
+
+ return res_cast
+
+ if lhs._Cr is None:
+ return lhs.solve_schur_no_R(
+ rhs,
+ tol=tol,
+ max_iters=max_iters,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+
+ u_rhs, f, w_lambda, c_k = rhs
+
+ A = lhs._A
+ H = lhs._H
+ W_skew = lhs._W
+ B = lhs._B
+ CSk = lhs._Cs
+ CRk = lhs._Cr
+
+ u_matrix = sp.bsr_copy(A)
+
+ H_inv = wp.empty_like(H.values)
+ W_skew_inv = wp.empty_like(W_skew.values)
+ CHiCt_inv = wp.empty(
+ shape=H.nrow, dtype=wp.mat(shape=(9, 9), dtype=H.scalar_type)
+ )
+ lambda_rhs = wp.clone(c_k, requires_grad=False)
+
+ wp.launch(
+ invert_blocks,
+ dim=W_skew.nnz,
+ inputs=[W_skew.values, W_skew_inv],
+ device=W_skew.device,
+ )
+ wp.launch(
+ invert_blocks,
+ dim=H.nnz,
+ inputs=[H.values, H_inv],
+ device=H.device,
+ )
+
+ wp.launch(
+ kernel=compute_first_schur,
+ dim=CHiCt_inv.shape,
+ inputs=[
+ CHiCt_inv,
+ CSk.values,
+ CRk.values,
+ H_inv,
+ W_skew_inv,
+ lambda_rhs,
+ f,
+ w_lambda,
+ ],
+ )
+
+ BtCHiCt_inv = sp.bsr_transposed(B) if lhs._Bt is None else sp.bsr_copy(lhs._Bt)
+ wp.launch(
+ bsr_mul_diag,
+ dim=BtCHiCt_inv.nnz,
+ inputs=[BtCHiCt_inv.values, BtCHiCt_inv.columns, CHiCt_inv],
+ )
+
+ sp.bsr_mm(
+ x=BtCHiCt_inv,
+ y=B,
+ z=u_matrix,
+ alpha=1.0,
+ beta=1.0,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+ sp.bsr_mv(A=BtCHiCt_inv, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ delta_du = wp.zeros_like(u_rhs)
+ err, niter = bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=tol, max_iters=max_iters, quiet=True
+ )
+
+ if np.isnan(err):
+ raise RuntimeError(f"Solver fail, rhs= {np.linalg.norm(u_rhs.numpy())}")
+
+ # other variable updates
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ sp.bsr_mv(A=B, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+ dLambda = wp.empty_like(lambda_rhs)
+ dS = wp.empty_like(f)
+ dR = wp.empty_like(w_lambda)
+
+ wp.launch(
+ kernel=compute_dLambdadRdS,
+ dim=H_inv.shape[0],
+ inputs=[
+ CSk.values,
+ CRk.values,
+ CHiCt_inv,
+ H_inv,
+ W_skew_inv,
+ lambda_rhs,
+ f,
+ w_lambda,
+ dLambda,
+ dS,
+ dR,
+ ],
+ )
+
+ return delta_du, dS, dR, dLambda
+
+ def solve_schur_no_R(
+ lhs,
+ rhs: Tuple,
+ tol=1.0e-8,
+ max_iters=1000,
+ work_arrays=None,
+ reuse_topology=False,
+ ):
+ u_rhs, f, w_lambda, c_k = rhs
+
+ A = lhs._A
+ H = lhs._H
+ B = lhs._B
+ CSk = lhs._Cs
+
+ u_matrix = sp.bsr_copy(A)
+
+ Cs_inv = sp.bsr_copy(CSk)
+ wp.launch(
+ invert_blocks,
+ dim=Cs_inv.nnz,
+ inputs=[CSk.values, Cs_inv.values],
+ device=H.device,
+ )
+
+ CHiCt_inv = wp.empty(
+ shape=H.nrow, dtype=wp.mat(shape=(6, 6), dtype=H.scalar_type)
+ )
+
+ wp.launch(
+ kernel=compute_first_schur_no_R,
+ dim=CHiCt_inv.shape,
+ inputs=[
+ CHiCt_inv,
+ Cs_inv.values,
+ H.values,
+ ],
+ )
+
+ ci_f = Cs_inv @ f
+ Bt = sp.bsr_transposed(B) if lhs._Bt is None else sp.bsr_copy(lhs._Bt)
+ sp.bsr_mv(A=Bt, x=ci_f, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ BtCHiCt_inv = Bt
+ wp.launch(
+ bsr_mul_diag,
+ dim=BtCHiCt_inv.nnz,
+ inputs=[BtCHiCt_inv.values, BtCHiCt_inv.columns, CHiCt_inv],
+ )
+
+ sp.bsr_mm(
+ x=BtCHiCt_inv,
+ y=B,
+ z=u_matrix,
+ alpha=1.0,
+ beta=1.0,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+ sp.bsr_mv(A=BtCHiCt_inv, x=c_k, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ delta_du = wp.zeros_like(u_rhs)
+ err, niter = bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=tol, max_iters=max_iters, quiet=True
+ )
+
+ if np.isnan(err):
+ raise RuntimeError(f"Solver fail, rhs= {np.linalg.norm(u_rhs.numpy())}")
+
+ # other variable updates
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ lambda_rhs = wp.clone(c_k, requires_grad=False)
+ sp.bsr_mv(A=B, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ dLambda = wp.empty_like(lambda_rhs)
+ dS = wp.empty_like(f)
+ dR = wp.empty_like(w_lambda)
+
+ wp.launch(
+ kernel=compute_dLambdadS,
+ dim=Cs_inv.values.shape[0],
+ inputs=[
+ Cs_inv.values,
+ CHiCt_inv,
+ lambda_rhs,
+ ci_f,
+ dLambda,
+ dS,
+ ],
+ )
+
+ return delta_du, dS, dR, dLambda
+
+
+@wp.func
+def invert_schur_block(M: Any):
+ eps = type(M[0])(M.dtype(1.0e-16))
+ return inverse_qr(M + wp.diag(eps))
+
+
+@wp.kernel
+def compute_first_schur(
+ CHiC_inv: wp.array(dtype=Any),
+ Cs: wp.array(dtype=Any),
+ Cr: wp.array(dtype=Any),
+ H_inv: wp.array(dtype=Any),
+ W_inv: wp.array(dtype=Any),
+ lambda_rhs: wp.array(dtype=Any),
+ f: wp.array(dtype=Any),
+ w_lambda: wp.array(dtype=Any),
+):
+ i = wp.tid()
+
+ cr = Cr[i]
+ cs = Cs[i]
+
+ csHi = cs * H_inv[i]
+ crWi = cr * W_inv[i]
+
+ lambda_rhs[i] += csHi * f[i] + crWi * w_lambda[i]
+
+ CHiC = csHi * wp.transpose(cs) + crWi * wp.transpose(cr)
+ CHiC_inv[i] = invert_schur_block(CHiC)
+
+
+@wp.kernel
+def compute_dLambdadRdS(
+ Cs: wp.array(dtype=Any),
+ Cr: wp.array(dtype=Any),
+ C_inv: wp.array(dtype=Any),
+ H_inv: wp.array(dtype=Any),
+ W_inv: wp.array(dtype=Any),
+ lambda_rhs: wp.array(dtype=Any),
+ f: wp.array(dtype=Any),
+ w_lambda: wp.array(dtype=Any),
+ dLambda: wp.array(dtype=Any),
+ dS: wp.array(dtype=Any),
+ dR: wp.array(dtype=Any),
+):
+ i = wp.tid()
+ dL = -C_inv[i] * lambda_rhs[i]
+ dLambda[i] = dL
+ dS[i] = -H_inv[i] * (f[i] + wp.transpose(Cs[i]) * dL)
+ dR[i] = -W_inv[i] * (w_lambda[i] + wp.transpose(Cr[i]) * dL)
+
+
+@wp.kernel
+def compute_first_schur_no_R(
+ CHiC_inv: wp.array(dtype=Any),
+ Csi: wp.array(dtype=Any),
+ H: wp.array(dtype=Any),
+):
+ i = wp.tid()
+
+ CHiC_inv[i] = Csi[i] * H[i] * Csi[i]
+
+
+@wp.kernel
+def compute_dLambdadS(
+ Csi: wp.array(dtype=Any),
+ CHiC_inv: wp.array(dtype=Any),
+ lambda_rhs: wp.array(dtype=Any),
+ ci_f: wp.array(dtype=Any),
+ dLambda: wp.array(dtype=Any),
+ dS: wp.array(dtype=Any),
+):
+ i = wp.tid()
+
+ dLambda[i] = -CHiC_inv[i] * lambda_rhs[i] - ci_f[i]
+ dS[i] = Csi[i] * lambda_rhs[i]
+
+
+@wp.kernel
+def invert_blocks(A: wp.array(dtype=Any), A_inv: wp.array(dtype=Any)):
+ i = wp.tid()
+ A_inv[i] = inverse_qr(A[i])
+
+
+@wp.kernel
+def invert_schur_blocks(values: wp.array(dtype=Any)):
+ i = wp.tid()
+
+ values[i] = invert_schur_block(values[i])
+
+
+@wp.kernel
+def bsr_mul_diag(
+ Bt_values: wp.array(dtype=Any),
+ Bt_columns: wp.array(dtype=int),
+ C_values: wp.array(dtype=Any),
+):
+ i = wp.tid()
+ col = Bt_columns[i]
+ Bt_values[i] *= C_values[col]
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_2d.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..afa342ff9f48fba9a019864102c1ef4fcd065a3c
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_2d.py
@@ -0,0 +1,1664 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+
+import warp as wp
+
+import numpy as np
+
+from warp.fem import Domain, Sample, Field
+from warp.fem import normal, integrand, grad
+import warp.fem as fem
+
+from warp.fem.utils import array_axpy
+from warp.sparse import bsr_transposed, bsr_mm, bsr_axpy, bsr_mv, bsr_copy
+
+import warp.examples.fem.utils as fem_example_utils
+
+import matplotlib.pyplot as plt
+import matplotlib.animation as animation
+
+import math
+
+_SQRT_2 = wp.constant(math.sqrt(2.0))
+_SQRT_1_2 = wp.constant(math.sqrt(1.0 / 2.0))
+
+
+class FullTensorMapper(fem.DofMapper):
+ """Orthonormal isomorphism from R^{n (n+1)} to nxn symmetric tensors,
+ using usual L2 norm for vectors and half Frobenius norm, (tau : tau)/2 for tensors.
+ """
+
+ def __init__(self, dtype: type):
+ self.value_dtype = dtype
+ self.DOF_SIZE = wp.constant(4)
+ self.dof_dtype = wp.vec4
+
+ def __str__(self):
+ return f"_{self.DOF_SIZE}"
+
+ @wp.func
+ def dof_to_value(dof: wp.vec4):
+ a = _SQRT_2 * dof[0]
+ b = _SQRT_2 * dof[1]
+ c = dof[2]
+ d = dof[3]
+ return wp.mat22(a, c - d, c + d, b)
+
+ @wp.func
+ def value_to_dof(val: wp.mat22):
+ a = _SQRT_1_2 * val[0, 0]
+ b = _SQRT_1_2 * val[1, 1]
+ c = 0.5 * (val[0, 1] + val[1, 0])
+ d = 0.5 * (val[1, 0] - val[0, 1])
+ return wp.vec4(a, b, c, d)
+
+
+@wp.func
+def hooke_stress(strain: wp.mat22, lame: wp.vec2):
+ return 2.0 * lame[1] * strain + lame[0] * wp.trace(strain) * wp.identity(
+ n=2, dtype=float
+ )
+
+
+@integrand
+def linear_elasticity_hessian_form(
+ s: Sample, S: Field, tau: Field, sig: Field, lame: wp.vec2
+):
+ return wp.ddot(hooke_stress(sig(s), lame), tau(s))
+
+
+@integrand
+def linear_elasticity_gradient_form(s: Sample, tau: Field, S: Field, lame: wp.vec2):
+ return wp.ddot(hooke_stress(S(s) - wp.identity(n=2, dtype=float), lame), tau(s))
+
+
+@integrand
+def linear_elasticity_energy(s: Sample, S: Field, lame: wp.vec2):
+ strain = S(s) - wp.identity(n=2, dtype=float)
+ return 0.5 * wp.ddot(strain, hooke_stress(strain, lame))
+
+
+@wp.func
+def nh_parameters_from_lame(lame: wp.vec2):
+ """Parameters such that for small strains model behaves according to Hooke's law"""
+ mu_nh = lame[1]
+ lambda_nh = lame[0] + lame[1]
+
+ return mu_nh, lambda_nh
+
+
+@wp.func
+def nh_energy(F: wp.mat22, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ return 0.5 * lambda_nh * (J - gamma) * (J - gamma) + 0.5 * mu_nh * wp.ddot(F, F)
+
+
+@wp.func
+def nh_stress(F: wp.mat22, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ dJ_dF = wp.mat22(F[1, 1], -F[1, 0], -F[0, 1], F[0, 0])
+ return mu_nh * F + (lambda_nh * (J - gamma)) * dJ_dF
+
+
+@integrand
+def nh_elasticity_hessian_form(
+ s: Sample, S: Field, tau: Field, sig: Field, lame: wp.vec2
+):
+ tau_s = tau(s)
+ sig_s = sig(s)
+
+ F_s = S(s)
+ dJ_dF = wp.mat22(F_s[1, 1], -F_s[1, 0], -F_s[0, 1], F_s[0, 0])
+
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+
+ dpsi_dpsi = mu_nh * wp.ddot(tau_s, sig_s) + lambda_nh * wp.ddot(
+ dJ_dF, tau_s
+ ) * wp.ddot(dJ_dF, sig_s)
+
+ # SPD projection of (J - gamma) d2J_dS2
+ gamma = 1.0 + mu_nh / lambda_nh
+ J = wp.determinant(F_s)
+
+ d2J_dF_pos = wp.mat22(
+ sig_s[1, 1] + sig_s[0, 0],
+ sig_s[0, 1] - sig_s[1, 0],
+ sig_s[1, 0] - sig_s[0, 1],
+ sig_s[0, 0] + sig_s[1, 1],
+ )
+ d2J_dF_neg = wp.mat22(
+ sig_s[1, 1] - sig_s[0, 0],
+ -sig_s[0, 1] - sig_s[1, 0],
+ -sig_s[1, 0] - sig_s[0, 1],
+ sig_s[0, 0] - sig_s[1, 1],
+ )
+
+ d2J_dF = wp.min(0.5 * lambda_nh * (J - gamma), mu_nh) * d2J_dF_neg
+ d2J_dF += wp.max(0.5 * lambda_nh * (J - gamma), -mu_nh) * d2J_dF_pos
+
+ return dpsi_dpsi + wp.ddot(d2J_dF, tau_s)
+
+
+@integrand
+def nh_elasticity_gradient_form(s: Sample, tau: Field, S: Field, lame: wp.vec2):
+ return wp.ddot(tau(s), nh_stress(S(s), lame))
+
+
+@integrand
+def nh_elasticity_energy(s: Sample, S: Field, lame: wp.vec2):
+ return nh_energy(S(s), lame)
+
+
+@integrand
+def tensor_mass_form(s: Sample, sig: Field, tau: Field):
+ """
+ Mass form over tensor space
+ sig : tau
+ """
+ return wp.ddot(sig(s), tau(s))
+
+
+@integrand
+def boundary_projector_form(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = normal(domain, s)
+ clamped = float(0.0)
+
+ # Single clamped point
+ if s.qp_index == 0:
+ clamped = 1.0
+
+ # clamped vertical sides
+ # clamped = wp.abs(nor[0])
+
+ # clamped right sides
+ # clamped = wp.max(0.0, nor[0])
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def boundary_displacement_form(
+ s: Sample,
+ domain: Domain,
+ v: Field,
+ displacement: float,
+):
+ """Prescribed displacement"""
+
+ # opposed to normal
+ nor = normal(domain, s)
+ return -displacement * wp.dot(nor, v(s))
+
+
+@integrand
+def inertia_form(s: Sample, u: Field, v: Field, rho: float, dt: float):
+ """"""
+
+ u_rhs = rho * u(s) / (dt * dt)
+ return wp.dot(u_rhs, v(s))
+
+
+@integrand
+def dg_penalty_form(s: Sample, domain: Domain, u: Field, v: Field, k: float):
+ ju = fem.jump(u, s)
+ jv = fem.jump(v, s)
+
+ return wp.dot(ju, jv) * k / 10.0 * fem.measure_ratio(domain, s)
+
+
+@integrand
+def displacement_rhs_form(
+ s: Sample, u_cur: Field, u: Field, v: Field, rho: float, gravity: wp.vec2, dt: float
+):
+ """ + """
+
+ return (
+ inertia_form(s, u, v, rho, dt)
+ - inertia_form(s, u_cur, v, rho, dt)
+ + rho * wp.dot(gravity, v(s))
+ )
+
+
+@integrand
+def kinetic_potential_energy(
+ s: Sample, u: Field, v: Field, rho: float, dt: float, gravity: wp.vec2
+):
+ du = u(s)
+ dv = v(s)
+ return rho * (0.5 * wp.dot(du - dv, du - dv) / (dt * dt) - wp.dot(du, gravity))
+
+
+@wp.func
+def rotation_matrix(angle: float):
+ # return wp.identity(n=2, dtype=wp.float32)
+
+ c = wp.cos(angle)
+ s = wp.sin(angle)
+ return wp.mat22(c, -s, s, c)
+
+
+@wp.kernel
+def apply_rotation_delta(
+ R: wp.array(dtype=float), dR: wp.array(dtype=float), alpha: float
+):
+ i = wp.tid()
+ R[i] = R[i] + dR[i] * alpha
+
+
+class MFEM:
+ def __init__(self, args):
+ self.args = args
+
+ if args.grid:
+ self.geo = fem.Grid2D(
+ res=wp.vec2i(args.resolution), bounds_lo=wp.vec2(0.0, 0.75)
+ )
+ else:
+ positions, tri_vidx = fem_example_utils.gen_trimesh(
+ res=wp.vec2i(args.resolution), bounds_lo=wp.vec2(0.0, 0.75)
+ )
+ self.geo = fem.Trimesh2D(tri_vertex_indices=tri_vidx, positions=positions)
+
+ print("Cell area", 0.25 / self.geo.cell_count())
+
+ # Strain-stress matrix
+ young = args.young_modulus
+ poisson = args.poisson_ratio
+ self.lame = wp.vec2(
+ young / (1.0 + poisson) * np.array([poisson / (1.0 - poisson), 0.5])
+ )
+
+ self.dt = args.dt
+ self.gravity = wp.vec2(0.0, -args.gravity)
+ self.rot_stiff = 1.0 / args.rot_compliance
+
+ if args.grid:
+ self.strain_degree = args.degree
+ self.rot_degree = args.degree
+ self.strain_basis = fem.ElementBasis.LAGRANGE
+ else:
+ self.strain_degree = args.degree - 1
+ self.rot_degree = args.degree - 1
+ self.strain_basis = fem.ElementBasis.NONCONFORMING_POLYNOMIAL
+
+ self.strain_poly = fem.Polynomial.GAUSS_LEGENDRE
+
+ if args.neo_hookean:
+ self.elastic_energy_form = nh_elasticity_energy
+ self.elastic_gradient_form = nh_elasticity_gradient_form
+ self.elastic_hessian_form = nh_elasticity_hessian_form
+ else:
+ self.elastic_energy_form = linear_elasticity_energy
+ self.elastic_gradient_form = linear_elasticity_gradient_form
+ self.elastic_hessian_form = linear_elasticity_hessian_form
+
+ def init_vel_space(self):
+ # Function spaces -- Q_k for displacement, Q_{k-1}d for stress
+ u_space = fem.make_polynomial_space(
+ self.geo,
+ degree=args.degree,
+ dtype=wp.vec2,
+ discontinuous=False,
+ element_basis=(
+ fem.ElementBasis.SERENDIPITY if self.args.serendipity else None
+ ),
+ )
+
+ # Defines some fields over our function spaces
+ self.u_field = u_space.make_field() # displacement
+ self.du_field = u_space.make_field() # displacement delta
+ self.du_prev = u_space.make_field() # displacement delta
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ domain = fem.Cells(self.geo)
+ self.u_trial = fem.make_trial(space=u_space, domain=domain)
+ self.u_test = fem.make_test(space=u_space, domain=domain)
+
+ sides = fem.Sides(self.geo)
+ self.u_side_trial = fem.make_trial(space=u_space, domain=sides)
+ self.u_side_test = fem.make_test(space=u_space, domain=sides)
+
+ def init_strain_spaces(self):
+ args = self.args
+
+ # Store stress degrees of freedom as symmetric tensors (3 dof) rather than full 2x2 matrices
+ sym_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ dof_mapper=fem.SymmetricTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ # Function spaces for piecewise-constant per-element rotations and rotation vectors
+ rot_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.rot_degree,
+ discontinuous=True,
+ dtype=float,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+ skew_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.rot_degree,
+ dof_mapper=fem.SkewSymmetricTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ # Defines some fields over our function spaces
+
+ self.S = sym_space.make_field() # Rotated symmetric train
+ self.S.dof_values.fill_(
+ sym_space.dof_mapper.value_to_dof(wp.mat22(1.0, 0.0, 0.0, 1.0))
+ ) # initialize with identity
+
+ self.R = rot_space.make_field() # Rotation
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ domain = fem.Cells(self.geo)
+ self.sym_test = fem.make_test(space=sym_space, domain=domain)
+ self.sym_trial = fem.make_trial(
+ space=sym_space,
+ space_partition=self.sym_test.space_partition,
+ domain=domain,
+ )
+
+ if skew_space.degree == sym_space.degree:
+ self.skew_test = fem.make_test(
+ space=skew_space,
+ space_partition=self.sym_test.space_partition,
+ domain=domain,
+ )
+ self.skew_trial = fem.make_trial(
+ space=skew_space,
+ space_partition=self.sym_test.space_partition,
+ domain=domain,
+ )
+ else:
+ self.skew_test = fem.make_test(space=skew_space, domain=domain)
+ self.skew_trial = fem.make_trial(space=skew_space, domain=domain)
+
+ self.quadrature = fem.RegularQuadrature(domain, order=2 * args.degree)
+
+ def init_boundary_conditions(self):
+ u_space = self.u_field.space
+
+ # Displacement boundary conditions
+ # For simplicity, assume constant per-frame displacement
+ boundary = fem.BoundarySides(self.geo)
+ u_bd_test = fem.make_test(space=u_space, domain=boundary)
+ u_bd_trial = fem.make_trial(space=u_space, domain=boundary)
+ self.v_bd_rhs = fem.integrate(
+ boundary_displacement_form,
+ fields={"v": u_bd_test},
+ values={"displacement": args.displacement / args.n_frames},
+ nodal=True,
+ output_dtype=wp.vec2f,
+ )
+ self.v_bd_matrix = fem.integrate(
+ boundary_projector_form,
+ fields={"u": u_bd_trial, "v": u_bd_test},
+ nodal=True,
+ output_dtype=float,
+ )
+ fem.normalize_dirichlet_projector(self.v_bd_matrix, self.v_bd_rhs)
+
+ def init_constant_forms(self):
+ self.A = fem.integrate(
+ inertia_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt},
+ output_dtype=float,
+ ) + fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_side_trial, "v": self.u_side_test},
+ values={"k": self.lame[0]},
+ output_dtype=float,
+ )
+
+ self.Ci = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.constraint_test, "sig": self.constraint_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ fem_example_utils.invert_diagonal_bsr_matrix(self.Ci)
+
+ def run_frame(self):
+ (self.du_field, self.du_prev) = (self.du_prev, self.du_field)
+
+ self.compute_initial_guess()
+
+ tol = 1.0e-8
+
+ for k in range(self.args.n_newton):
+ E_ref = self.evaluate_energy()
+
+ ddu, dR, dS = self.newton_iter(k)
+ self.apply_newton_deltas(ddu, dR, dS)
+
+ step_size = wp.utils.array_inner(ddu, ddu) / (1 + ddu.shape[0])
+
+ # Line search
+ alpha = 1.0
+ for j in range(self.args.n_backtrack):
+ E_cur = self.evaluate_energy()
+ if E_cur < E_ref:
+ break
+
+ alpha = 0.5 * alpha
+ self.apply_newton_deltas(ddu, dR, dS, alpha=-alpha)
+
+ print(f"Newton iter {k}: step size {step_size}, alpha={alpha}")
+
+ if step_size < tol:
+ break
+
+ def assemble_constraint_free_system(self, with_external_forces=True):
+ gravity = self.gravity if with_external_forces else wp.vec2(0.0)
+
+ l = fem.integrate(
+ displacement_rhs_form,
+ fields={"u_cur": self.du_field, "u": self.du_prev, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt, "gravity": gravity},
+ output_dtype=wp.vec2,
+ )
+
+ pen_rhs = fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_side_test},
+ values={"k": self.lame[0]},
+ output_dtype=wp.vec2,
+ )
+ fem.utils.array_axpy(x=pen_rhs, y=l, alpha=-1, beta=1)
+
+ return self.A, l
+
+ def apply_newton_deltas(self, delta_du, dR, dS, alpha=1.0):
+ # Add to total displacement
+ array_axpy(x=delta_du, y=self.u_field.dof_values, alpha=alpha)
+ array_axpy(x=delta_du, y=self.du_field.dof_values, alpha=alpha)
+
+ array_axpy(x=dS, y=self.S.dof_values, alpha=alpha)
+
+ # Apply rotation delta
+ wp.launch(
+ kernel=apply_rotation_delta,
+ dim=self.R.space.node_count(),
+ inputs=[self.R.dof_values, dR, alpha],
+ )
+
+ def evaluate_energy(self, include_constraint_residual=True):
+ E_e = fem.integrate(
+ self.elastic_energy_form,
+ quadrature=fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * self.args.degree
+ ),
+ fields={"S": self.S},
+ values={"lame": self.lame},
+ )
+ E_u = fem.integrate(
+ kinetic_potential_energy,
+ quadrature=self.quadrature,
+ fields={"u": self.du_field, "v": self.du_prev},
+ values={"rho": self.args.density, "dt": self.dt, "gravity": self.gravity},
+ )
+ E_pen = fem.integrate(
+ dg_penalty_form,
+ domain=fem.Sides(self.geo),
+ fields={"u": self.u_field.trace(), "v": self.u_field.trace()},
+ values={"k": self.lame[0]},
+ )
+
+ E_tot = E_u + E_e + E_pen
+
+ if include_constraint_residual:
+ ck_field = self.evaluate_ck()
+
+ ck_field.dof_values = self.Ci @ ck_field.dof_values
+
+ c_r = (
+ fem.integrate(
+ tensor_mass_form,
+ quadrature=self.quadrature,
+ fields={"sig": ck_field, "tau": ck_field},
+ )
+ * self.lame[0]
+ )
+ E_tot += c_r
+
+ return E_tot
+
+ def compute_initial_guess(self):
+ # Self-advect
+ A, l = self.assemble_constraint_free_system(with_external_forces=False)
+ u_rhs = l
+ u_matrix = bsr_copy(self.A)
+
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, self.v_bd_rhs, normalize_projector=False
+ )
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=self.du_field.dof_values, quiet=True
+ )
+ array_axpy(x=self.du_field.dof_values, y=self.u_field.dof_values)
+
+
+class MFEM_S_RF(MFEM):
+ """S = RF variant"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.W = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.sym_test, "sig": self.sym_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_inv = bsr_copy(self.W)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_inv)
+
+ self.W_skew = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.skew_test, "sig": self.skew_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_skew_inv = bsr_copy(self.W_skew)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_skew_inv)
+
+ def newton_iter(self, k: int):
+ # argmin_u,S,R argmax lambda V(u, u) + psi(S) + c(dR) + w(S, lambda) - b(R, u, lambda)
+ #
+ # with lambda = lambda_sym + lambda_skew
+ #
+ # a(dU, v) - b(R, lambda, v) = l(v) - a(u, v forall v in R^3
+ # w(dS, tau) - b(R, dU, tau) = b(R, u, tau) - w(S, tau) forall tau in sym(3x3)
+ # -b(R, dU, tau) - b(dR, u, tau) = b(R, u, tau) forall tau in skew(3x3)
+ # h(S; dS, tau) + w(tau, lambda) = -f(S; tau) forall tau in sym(3x3)
+ # w(dR, tau) - b(tau, u, lambda) = 0 forall tau in skew(3x3)
+ #
+ # a(u, v) = int( rho/dt )
+ # l(v) = int( <(rho/dt v^0 + rho g), v> )
+ #
+ # b(R, u, tau) = int ( R (I + grad u) : tau)
+ # w(sig, tau) = int ( sig : tau)
+ #
+ # H(S; sig, tau) = int ( sig : (d2 psi/dS2)(S) : tau )
+ # f(S; tau) = int ( (dpsi / dS)(S) : tau )
+ #
+ # Notes:
+ # In general there should also be a b(dR, u, tau) term for tau symmetric,
+ # which will be zero if the deviatoric part of RF is zero
+ # w(dR, tau) is artificial inertia on dR (Tikhonov regularization)
+
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+
+ bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ RFk = fem.integrate(
+ self.rotated_defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "R": self.R, "tau": self.sym_test},
+ output_dtype=wp.vec3,
+ )
+ RBk = fem.integrate(
+ self.rotated_dispgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_trial, "R": self.R, "tau": self.sym_test},
+ output_dtype=float,
+ )
+
+ # Optional -- reset S as symmetric part of RFk
+ self.S.dof_values = self.W_inv @ RFk
+
+ Sk = fem.integrate(
+ tensor_mass_form,
+ # quadrature=self.quadrature,
+ nodal=True,
+ fields={"sig": self.S, "tau": self.sym_test},
+ output_dtype=wp.vec3,
+ )
+
+ # c_k -- sym part of constraint residual (RFk - WS)
+ c_k = RFk
+ array_axpy(Sk, c_k, alpha=-1.0, beta=1.0)
+
+ # Elasticity
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "sig": self.sym_trial, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec3,
+ )
+
+ # Schur complements
+
+ # lambda_rhs = H W^-1 c_k + f
+ lambda_rhs = f
+ bsr_mv(A=H, x=self.W_inv @ c_k, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ WiRBk = self.W_inv @ RBk
+ WiRBk_T = WiRBk.transpose()
+
+ bsr_mv(A=WiRBk_T, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix += WiRBk_T @ H @ WiRBk
+
+ # Rotation
+ RFk_skew = fem.integrate(
+ self.rotated_defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "R": self.R, "tau": self.skew_test},
+ output_dtype=float,
+ )
+
+ RBk_skew = fem.integrate(
+ self.rotated_dispgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_trial, "R": self.R, "tau": self.skew_test},
+ output_dtype=float,
+ )
+
+ # print(
+ # "RES ",
+ # wp.utils.array_inner(RFk_skew, bsr_mv(self.W_skew_inv, RFk_skew).view(dtype=RFk_skew.dtype)),
+ # wp.utils.array_inner(c_k, bsr_mv(self.W_inv, c_k).view(dtype=c_k.dtype)),
+ # )
+
+ Ck = fem.integrate(
+ self.defgrad_incremental_rotation_form,
+ fields={
+ "u": self.u_field,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.skew_test,
+ },
+ nodal=True,
+ output_dtype=float,
+ )
+ WiCt = self.rot_stiff * self.W_skew_inv @ Ck.transpose()
+
+ CWiCt_inv = Ck @ WiCt
+ fem_example_utils.invert_diagonal_bsr_matrix(CWiCt_inv)
+
+ RBk_skew_TCi = RBk_skew.transpose() @ CWiCt_inv
+ u_matrix += RBk_skew_TCi @ RBk_skew
+ bsr_mv(A=RBk_skew_TCi, x=RFk_skew, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # update S
+ # -RBk du + W dS = c_k
+ # W dS = c_k + RBK du
+ bsr_mv(A=RBk, x=delta_du, y=c_k, alpha=1.0, beta=1.0)
+ dS = self.W_inv @ c_k
+
+ # update R
+ #
+ # -RBk_skew du - CWiCt dlambda = RFk
+ # cW dR - Ct dlambda = 0
+ bsr_mv(A=RBk_skew, x=delta_du, y=RFk_skew, alpha=1.0, beta=1.0)
+ lambda_skew = -CWiCt_inv @ RFk_skew
+ dR = WiCt @ lambda_skew
+
+ return delta_du, dR, dS
+
+ @integrand
+ def constraint_residual(
+ s: Sample,
+ S: Field,
+ R: Field,
+ u: Field,
+ ):
+ """
+ Constraint residual
+ """
+ RFs = rotation_matrix(R(s)) * (grad(u, s) + wp.identity(n=2, dtype=float))
+ c = S(s) - RFs
+
+ return 0.5 * wp.ddot(c, c)
+
+ @integrand
+ def rotated_dispgrad_form(
+ s: Sample,
+ R: Field,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Rotated displacement gradient form
+ R grad(u) : tau
+ """
+ return wp.ddot(wp.transpose(tau(s)), rotation_matrix(R(s)) * grad(u, s))
+
+ @integrand
+ def rotated_defgrad_form(
+ s: Sample,
+ R: Field,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Rotated deformation gradient form
+ R (I + grad(u)) : tau
+ """
+ return wp.ddot(
+ wp.transpose(tau(s)),
+ rotation_matrix(R(s)) * (wp.identity(n=2, dtype=float) + grad(u, s)),
+ )
+
+ @integrand
+ def defgrad_incremental_rotation_form(
+ s: Sample, R: Field, dR: Field, u: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R dR grad(u) : tau
+ """
+ return wp.ddot(
+ rotation_matrix(R(s))
+ * dR(s)
+ * (wp.identity(n=2, dtype=float) + grad(u, s)),
+ wp.transpose(tau(s)),
+ )
+
+
+class MFEM_RS_F(MFEM):
+ """RS = F variant"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ # self.rot_stiff = self.rot_stiff / self.lame[0]
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces()
+
+ constraint_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ # dtype=wp.mat22,
+ dof_mapper=FullTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ self.constraint_test = fem.make_test(
+ space=constraint_space, space_partition=self.sym_test.space_partition
+ )
+ self.constraint_trial = fem.make_trial(
+ space=constraint_space, space_partition=self.sym_test.space_partition
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.quadrature = fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * (args.degree - 1)
+ )
+
+ u_sides = fem.make_trial(self.u_trial.space, domain=fem.Sides(self.geo))
+ tau_sides = fem.make_test(
+ self.constraint_test.space, domain=fem.Sides(self.geo)
+ )
+
+ self.B = fem.integrate(
+ self.dispgrad_form,
+ fields={"tau": self.constraint_test, "u": self.u_trial},
+ output_dtype=float,
+ ) + fem.integrate(
+ self.dispgrad_side_form,
+ fields={"tau": tau_sides, "u": u_sides},
+ output_dtype=float,
+ )
+
+ self.Bt = bsr_transposed(self.B)
+
+ self.W_skew = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.skew_test, "sig": self.skew_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_skew_inv = bsr_copy(self.W_skew)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_skew_inv)
+
+ def evaluate_ck(self):
+ tau_sides = fem.make_test(
+ self.constraint_test.space, domain=fem.Sides(self.geo)
+ )
+
+ Fk = fem.integrate(
+ self.defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+ fem.utils.array_axpy(
+ y=Fk,
+ x=fem.integrate(
+ self.dispgrad_side_form,
+ # quadrature=self.quadrature,
+ fields={"u": self.u_field.trace(), "tau": tau_sides},
+ output_dtype=wp.vec4,
+ ),
+ )
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ RSk = fem.integrate(
+ self.rotated_strain_form,
+ quadrature=self.quadrature,
+ fields={"sig": self.S, "R": self.R, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # c_k -- constraint residual (Fk - RS)
+ c_k = Fk
+ array_axpy(x=RSk, y=c_k, alpha=-1.0, beta=1.0)
+
+ ck_field = self.constraint_test.space.make_field()
+ ck_field.dof_values = c_k
+ return ck_field
+
+ def newton_iter(self, k: int):
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+ # bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ c_k = self.evaluate_ck().dof_values
+
+ # Grad of rotated strain w.r.t R, S
+ CSk = fem.integrate(
+ self.rotated_strain_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "R": self.R, "tau": self.constraint_test},
+ output_dtype=float,
+ )
+ CRk = fem.integrate(
+ self.incremental_strain_rotation_form,
+ nodal=True,
+ fields={
+ "sig": self.S,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.constraint_test,
+ },
+ output_dtype=float,
+ )
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={"S": self.S, "sig": self.sym_trial, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec3,
+ )
+
+ H_inv = bsr_copy(H)
+ wp.launch(
+ kernel=self.invert_hessian_blocks, dim=H_inv.nnz, inputs=[H_inv.values]
+ )
+
+ # Schur complements
+
+ CSkHi = CSk @ H_inv
+ CSt = CSk.transpose()
+
+ CHiCt = CSkHi @ CSt
+
+ # lambda_rhs = c_k + CS H^-1 f
+ lambda_rhs = c_k
+ bsr_mv(A=CSkHi, x=f, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ CRkWi = CRk @ (self.W_skew_inv * self.rot_stiff)
+ CRt = CRk.transpose()
+
+ CWiCt = CRkWi @ CRt
+
+ CHiCt_inv = bsr_copy(CHiCt, scalar_type=wp.float64) + bsr_copy(
+ CWiCt, scalar_type=wp.float64
+ )
+ wp.launch(
+ kernel=self.invert_schur_blocks,
+ dim=CHiCt_inv.nnz,
+ inputs=[CHiCt_inv.values],
+ )
+ CHiCt_inv = bsr_copy(CHiCt_inv, scalar_type=wp.float32)
+
+ BtCHiCt_inv = self.Bt @ CHiCt_inv
+
+ bsr_mm(x=BtCHiCt_inv, y=self.B, z=u_matrix, alpha=1.0, beta=1.0)
+ bsr_mv(A=BtCHiCt_inv, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ bsr_mv(A=self.B, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+ lambda_k = -CHiCt_inv @ lambda_rhs
+
+ # update S
+ # H dS + CSkT lambda = - f
+
+ bsr_mv(A=CSt, x=lambda_k, y=f, alpha=1.0, beta=1.0)
+ dS = -H_inv @ f
+
+ # update R
+ r = (CRt @ lambda_k).view(dtype=float)
+ dR = (-self.rot_stiff * self.W_skew_inv) @ r
+
+ return delta_du, dR, dS
+
+ @integrand
+ def constraint_residual(
+ domain: Domain,
+ s: Sample,
+ S: Field,
+ R: Field,
+ u: Field,
+ ):
+ """
+ Constraint residual
+ """
+ c = rotation_matrix(R(s)) * S(s) - (grad(u, s) + wp.identity(n=2, dtype=float))
+
+ return 0.5 * wp.ddot(c, c)
+
+ @integrand
+ def dispgrad_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau
+ """
+ return wp.ddot(tau(s), grad(u, s))
+
+ @integrand
+ def defgrad_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Deformation gradient form
+ (I + grad(u)) : tau
+ """
+ return wp.ddot(tau(s), (wp.identity(n=2, dtype=float) + grad(u, s)))
+
+ @integrand
+ def dispgrad_side_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau
+ """
+ grad_h = -wp.outer(fem.jump(u, s), fem.normal(domain, s))
+ return wp.ddot(tau(s), grad_h)
+
+ @integrand
+ def rotated_strain_form(
+ s: Sample, domain: Domain, R: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R S : tau
+ """
+ return wp.ddot(rotation_matrix(R(s)) * sig(s), tau(s))
+
+ @integrand
+ def incremental_strain_rotation_form(
+ s: Sample, domain: Domain, R: Field, dR: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R dR S : tau
+ """
+ return wp.ddot(rotation_matrix(R(s)) * dR(s) * sig(s), tau(s))
+
+ @wp.kernel
+ def invert_hessian_blocks(values: wp.array(dtype=(wp.mat33))):
+ i = wp.tid()
+ values[i] = wp.inverse(values[i])
+
+ @wp.kernel
+ def invert_schur_blocks(values: wp.array(dtype=(wp.mat44d))):
+ i = wp.tid()
+ values[i] = fem.utils.inverse_qr(values[i])
+
+ # @wp.kernel
+ # def pseudo_inverse_S(
+ # values: wp.array(dtype=(wp.mat(shape=(4, 3), dtype=wp.float32)))
+ # ):
+ # i = wp.tid()
+
+ # v = values[i]
+ # values[i] = v * wp.inverse(wp.transpose(v) * v)
+
+ # @wp.kernel
+ # def pseudo_inverse_R(
+ # values: wp.array(dtype=(wp.mat(shape=(4, 1), dtype=wp.float32)))
+ # ):
+ # i = wp.tid()
+
+ # v = values[i]
+ # values[i] = v / wp.ddot(v, v)
+
+ # @wp.kernel
+ # def add_coupling(
+ # CHCi: wp.array(dtype=(wp.mat(shape=(4, 4), dtype=wp.float32))),
+ # CS: wp.array(dtype=(wp.mat(shape=(4, 3), dtype=wp.float32))),
+ # CR: wp.array(dtype=(wp.mat(shape=(4, 1), dtype=wp.float32))),
+ # W: wp.array(dtype=float),
+ # rot_stiff: float
+ # ):
+ # i = wp.tid()
+
+ # cr = CR[i]
+ # cwc = cr * wp.transpose(cr) * rot_stiff / W[i]
+
+ # if i == 0:
+ # print(cwc)
+
+ # #cs = CS[i]
+ # #pis = cs * wp.transpose(cs) / wp.ddot(cs, cs)
+
+ # #CHCi[i] -= wp.ddot(pis, cwc) * pis
+ #
+ @wp.kernel
+ def compute_CHiCT_inv(
+ CHCi: wp.array(dtype=(wp.mat(shape=(4, 4), dtype=wp.float32))),
+ CS: wp.array(dtype=(wp.mat(shape=(4, 3), dtype=wp.float32))),
+ CR: wp.array(dtype=(wp.mat(shape=(4, 1), dtype=wp.float32))),
+ H: wp.array(dtype=(wp.mat(shape=(3, 3), dtype=wp.float32))),
+ W: wp.array(dtype=float),
+ rot_stiff: float,
+ ):
+ i = wp.tid()
+
+ cr = CR[i]
+ cs = CS[i]
+
+ pis = 3.0 * cs * wp.transpose(cs) / wp.ddot(cs, cs)
+ pir = wp.identity(n=4, dtype=wp.float32) - pis
+
+ cs_mp = cs * wp.inverse(wp.transpose(cs) * cs)
+ cihci = cs_mp * H[i] * wp.transpose(cs_mp)
+
+ cr_s = pis * cr
+ cr_r = cr - cr_s
+
+ wi = rot_stiff / W[i]
+
+ Ar = wi * cr_s * wp.transpose(cr_s)
+ A = cs * wp.inverse(H[i]) * wp.transpose(cs) + Ar
+ C = wi * cr_r * wp.transpose(cr_r)
+ B = wi * cr_r * wp.transpose(cr_s)
+
+ # A_mp = cihci - cihci * Ar * cihci
+ A_mp = pis * wp.inverse(A + pir) * pis
+
+ cr_mp = cr_r / wp.ddot(cr_r, cr_r)
+ C_mp = cr_mp * W[i] / rot_stiff * wp.transpose(cr_mp)
+
+ Ai = A_mp + A_mp * (wp.transpose(B) * C_mp * B) * A_mp
+ Ci = C_mp + C_mp * B * A_mp * wp.transpose(B) * C_mp
+
+ # AS = A - wp.transpose(B) * C_mp * B
+ # Ai = pis * wp.inverse(AS + pir) * pis
+
+ # CSsh = C - B * A_mp * wp.transpose(B)
+ # Ci = pir * wp.inverse(CSsh + pis) * pir
+
+ CHCi[i] = Ai + Ci - Ai * wp.transpose(B) * C_mp - Ci * B * A_mp
+
+ CHC = A + C + B + wp.transpose(B)
+ # CHCi[i] = wp.inverse(CHC)
+
+ if i == 0:
+ wp.print(CHCi[i] * CHC)
+
+
+class MFEM_S_RF_v2(MFEM):
+ """S = RF variant v2"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ # self.rot_stiff = self.rot_stiff / self.lame[0]
+
+ self.strain_form = tensor_mass_form
+ self.constraint_residual = MFEM_S_RF.constraint_residual
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces()
+
+ constraint_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ dof_mapper=FullTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ self.constraint_test = fem.make_test(
+ space=constraint_space, space_partition=self.sym_test.space_partition
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.quadrature = fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * (args.degree - 1)
+ )
+
+ self.W_skew = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.skew_test, "sig": self.skew_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_skew_inv = bsr_copy(self.W_skew)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_skew_inv)
+
+ def newton_iter(self, k: int):
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+ bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ # Deformation gradient Fl
+ RFk = fem.integrate(
+ MFEM_S_RF.rotated_defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "R": self.R, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+ RBk = fem.integrate(
+ MFEM_S_RF.rotated_dispgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_trial, "R": self.R, "tau": self.constraint_test},
+ output_dtype=float,
+ )
+ RBk_T = bsr_transposed(RBk)
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ Sk = fem.integrate(
+ self.strain_form,
+ # quadrature=self.quadrature,
+ nodal=True,
+ fields={"sig": self.S, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # c_k -- constraint residual (RFk - S)
+ c_k = RFk
+ array_axpy(x=Sk, y=c_k, alpha=-1.0, beta=1.0)
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={"S": self.S, "sig": self.sym_trial, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec3,
+ )
+
+ # Grad of rotated strain w.r.t R, S
+ CSk = fem.integrate(
+ self.strain_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "tau": self.constraint_test},
+ output_dtype=float,
+ )
+ CRk = fem.integrate(
+ MFEM_S_RF.defgrad_incremental_rotation_form,
+ fields={
+ "u": self.u_field,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.constraint_test,
+ },
+ nodal=True,
+ output_dtype=float,
+ )
+
+ H_inv = bsr_copy(H)
+ wp.launch(
+ kernel=self.invert_hessian_blocks, dim=H_inv.nnz, inputs=[H_inv.values]
+ )
+
+ # Schur complements
+
+ CSkHi = bsr_mm(CSk, H_inv)
+ CSt = bsr_transposed(CSk)
+
+ CHiCt = bsr_mm(CSkHi, CSt)
+
+ # lambda_rhs = c_k + CS H^-1 f
+ lambda_rhs = c_k
+ bsr_mv(A=CSkHi, x=f, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ CRkWi = bsr_mm(CRk, self.W_skew_inv, alpha=self.rot_stiff)
+ CRt = bsr_transposed(CRk)
+
+ CWiCt = bsr_mm(CRkWi, CRt, alpha=1.0, beta=1.0)
+
+ CHiCt_inv = bsr_axpy(
+ bsr_copy(CHiCt, scalar_type=wp.float64),
+ bsr_copy(CWiCt, scalar_type=wp.float64),
+ )
+ wp.launch(
+ kernel=self.invert_schur_blocks,
+ dim=CHiCt_inv.nnz,
+ inputs=[CHiCt_inv.values],
+ )
+ CHiCt_inv = bsr_copy(CHiCt_inv, scalar_type=wp.float32)
+
+ BtCHiCt_inv = bsr_mm(RBk_T, CHiCt_inv)
+
+ bsr_mm(x=BtCHiCt_inv, y=RBk, z=u_matrix, alpha=1.0, beta=1.0)
+ bsr_mv(A=BtCHiCt_inv, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ bsr_mv(A=RBk, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+ lambda_k = bsr_mv(CHiCt_inv, lambda_rhs, alpha=-1.0, beta=0.0)
+
+ # update S
+ # H dS + CSkT lambda = - f
+
+ bsr_mv(A=CSt, x=lambda_k, y=f, alpha=1.0, beta=1.0)
+ dS = bsr_mv(A=H_inv, x=f, alpha=-1.0, beta=0.0)
+
+ # update R
+ r = bsr_mv(A=CRt, x=lambda_k).view(dtype=float)
+ dR = bsr_mv(A=self.W_skew_inv, x=r, alpha=self.rot_stiff, beta=0.0)
+
+ return delta_du, dR, dS
+
+ @wp.kernel
+ def invert_hessian_blocks(values: wp.array(dtype=(wp.mat33))):
+ i = wp.tid()
+ values[i] = wp.inverse(values[i])
+
+ @wp.kernel
+ def invert_schur_blocks(values: wp.array(dtype=(wp.mat44d))):
+ i = wp.tid()
+ values[i] = fem.utils.inverse_qr(values[i])
+
+
+class MFEM_S_F(MFEM):
+ """S = F variant"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces()
+
+ constraint_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ dtype=wp.mat22,
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ self.constraint_test = fem.make_test(space=constraint_space)
+ self.constraint_trial = fem.make_trial(space=constraint_space)
+
+ self.S = constraint_space.make_field()
+ self.S.dof_values.fill_(
+ wp.mat22(1.0, 0.0, 0.0, 1.0)
+ ) # initialize with identity
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.quadrature = fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * args.degree - 1
+ )
+
+ self.B = fem.integrate(
+ self.dispgrad_form,
+ fields={"tau": self.constraint_test, "u": self.u_trial},
+ output_dtype=float,
+ )
+
+ self.W = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.constraint_test, "sig": self.constraint_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_inv = bsr_copy(self.W)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_inv)
+
+ self.WiB = self.W_inv @ self.B
+ self.BtWi = self.WiB.transpose()
+
+ def newton_iter(self, k: int):
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+ bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ # Deformation gradient Fl
+ Fk = fem.integrate(
+ self.defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # bsr_mv(self.W_inv, x=Fk, y=self.S.dof_values.view(dtype=wp.vec4), alpha=1.0, beta=0.0)
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ Sk = fem.integrate(
+ tensor_mass_form,
+ quadrature=self.quadrature,
+ fields={"sig": self.S, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # c_k -- constraint residual (Fk - S)
+ c_k = Fk
+ array_axpy(x=Sk, y=c_k, alpha=-1.0, beta=1.0)
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ quadrature=self.quadrature,
+ fields={
+ "S": self.S,
+ "sig": self.constraint_test,
+ "tau": self.constraint_trial,
+ },
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.constraint_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec4,
+ )
+
+ # Schur complement
+
+ # lambda_rhs = H W^-1 c_k + f
+ lambda_rhs = f
+ bsr_mv(A=H, x=bsr_mv(self.W_inv, c_k), y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ bsr_mv(A=self.BtWi, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+ bsr_mm(x=self.BtWi, y=bsr_mm(H, self.WiB), z=u_matrix, alpha=1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # update S
+ # -Bk du + W dS = c_k
+ # W dS = c_k + RBK du
+ bsr_mv(A=self.B, x=delta_du, y=c_k, alpha=1.0, beta=1.0)
+ dS = bsr_mv(A=self.W_inv, x=c_k, alpha=1.0, beta=0.0)
+ dS = dS.view(dtype=wp.mat22)
+
+ dR = self.skew_test.space.make_field().dof_values
+
+ return delta_du, dR, dS
+
+ @integrand
+ def constraint_residual(
+ s: Sample,
+ S: Field,
+ R: Field,
+ u: Field,
+ ):
+ """
+ Constraint residual
+ """
+ c = S(s) - (grad(u, s) + wp.identity(n=2, dtype=float))
+ return 0.5 * wp.ddot(c, c)
+
+ @integrand
+ def dispgrad_form(
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau
+ """
+ return wp.ddot(tau(s), grad(u, s))
+
+ @integrand
+ def defgrad_form(
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Deformation gradient form
+ (I + grad(u)) : tau
+ """
+ return wp.ddot(tau(s), (wp.identity(n=2, dtype=float) + grad(u, s)))
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+ wp.set_module_options({"enable_backward": False})
+ wp.set_module_options({"max_unroll": 2})
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument("--degree", type=int, default=1)
+ parser.add_argument("--serendipity", action="store_true", default=False)
+ parser.add_argument("--displacement", type=float, default=0.0)
+ parser.add_argument("-n", "--n_frames", type=int, default=25)
+ parser.add_argument("--n_newton", type=int, default=2)
+ parser.add_argument("--n_backtrack", type=int, default=4)
+ parser.add_argument("--young_modulus", type=float, default=100.0)
+ parser.add_argument("--poisson_ratio", type=float, default=0.5)
+ parser.add_argument("--gravity", type=float, default=10.0)
+ parser.add_argument("--density", type=float, default=1.0)
+ parser.add_argument("--dt", type=float, default=0.1)
+ parser.add_argument("--rot_compliance", type=float, default=0.001)
+ parser.add_argument("-v", "--variant", type=str, default="rs_f")
+ parser.add_argument("--grid", action="store_true", default=False)
+ parser.add_argument("-nh", "--neo_hookean", action="store_true", default=False)
+ args = parser.parse_args()
+
+ if args.variant == "rs_f":
+ sim = MFEM_RS_F(args)
+ elif args.variant == "s_rf":
+ sim = MFEM_S_RF(args)
+ elif args.variant == "s_rf2":
+ sim = MFEM_S_RF_v2(args)
+ elif args.variant == "s_f":
+ sim = MFEM_S_F(args)
+ else:
+ raise ValueError(f"Invalid variant: {args.variant}")
+
+ sim.init_vel_space()
+ sim.init_strain_spaces()
+ sim.init_boundary_conditions()
+ sim.init_constant_forms()
+
+ plot = fem_example_utils.Plot()
+ plot.add_field("u", sim.u_field)
+
+ for f in range(args.n_frames):
+ # TODO currently performing a single Newton iteration
+
+ print(f"--- Frame {f} ---")
+ sim.run_frame()
+ plot.add_field("u", sim.u_field)
+
+ plot.plot(
+ {
+ "u": {
+ "displacement": {},
+ "xlim": (-0.5, 1.5),
+ "ylim": (-0.25, 1.25),
+ },
+ },
+ backend="matplotlib",
+ )
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_3d.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f3fde9baa49877d15a7328877b9ee060a4f8a88
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_3d.py
@@ -0,0 +1,1332 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import math
+import gc
+from typing import Any, Optional
+
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+from warp.fem import Domain, Field, Sample
+from warp.fem.utils import array_axpy
+
+from fem_examples.mfem.linalg import MFEMSystem
+from fem_examples.mfem.softbody_sim import (
+ SoftbodySim,
+ defgrad,
+)
+
+from fem_examples.mfem.elastic_models import hooke_energy, hooke_stress, hooke_hessian
+from fem_examples.mfem.elastic_models import (
+ symmetric_strain,
+ symmetric_strain_delta,
+ snh_energy,
+ snh_stress,
+ snh_hessian_proj,
+)
+
+wp.set_module_options({"enable_backward": False})
+wp.set_module_options({"max_unroll": 4})
+wp.set_module_options({"fast_math": True})
+
+Scalar = wp.float32
+vec3s = wp.vec(length=3, dtype=Scalar)
+vec6s = wp.vec(length=6, dtype=Scalar)
+vec9s = wp.vec(length=9, dtype=Scalar)
+
+_SQRT_2 = wp.constant(math.sqrt(2.0))
+_SQRT_1_2 = wp.constant(math.sqrt(1.0 / 2.0))
+
+
+class FullTensorMapper(fem.DofMapper):
+ """Orthonormal isomorphism from R^{n (n+1)} to nxn symmetric tensors,
+ using usual L2 norm for vectors and half Frobenius norm, (tau : tau)/2 for tensors.
+ """
+
+ def __init__(self):
+ self.value_dtype = wp.mat33
+ self.DOF_SIZE = wp.constant(9)
+ self.dof_dtype = vec9s
+
+ def __str__(self):
+ return f"_{self.DOF_SIZE}"
+
+ @wp.func
+ def dof_to_value(dof: vec9s):
+ a = _SQRT_2 * dof[0]
+ b = _SQRT_2 * dof[1]
+ c = _SQRT_2 * dof[2]
+ d = dof[3]
+ e = dof[4]
+ f = dof[5]
+
+ ka = dof[6]
+ kb = dof[7]
+ kc = dof[8]
+ return wp.mat33(
+ a,
+ f - kc,
+ e + kb,
+ f + kc,
+ b,
+ d - ka,
+ e - kb,
+ d + ka,
+ c,
+ )
+
+ @wp.func
+ def value_to_dof(val: wp.mat33):
+ a = _SQRT_1_2 * val[0, 0]
+ b = _SQRT_1_2 * val[1, 1]
+ c = _SQRT_1_2 * val[2, 2]
+
+ d = 0.5 * (val[2, 1] + val[1, 2])
+ e = 0.5 * (val[0, 2] + val[2, 0])
+ f = 0.5 * (val[1, 0] + val[0, 1])
+
+ ka = 0.5 * (val[2, 1] - val[1, 2])
+ kb = 0.5 * (val[0, 2] - val[2, 0])
+ kc = 0.5 * (val[1, 0] - val[0, 1])
+
+ return vec9s(a, b, c, d, e, f, ka, kb, kc)
+
+
+@wp.func
+def rotation_matrix(rot_vec: wp.vec3):
+ quat = wp.quat_from_axis_angle(wp.normalize(rot_vec), wp.length(rot_vec))
+ return wp.quat_to_matrix(quat)
+
+
+@wp.kernel
+def apply_rotation_delta(
+ r_vec: wp.array(dtype=wp.vec3), dR: wp.array(dtype=wp.vec3), alpha: float
+):
+ i = wp.tid()
+
+ Q = wp.quat_from_axis_angle(wp.normalize(r_vec[i]), wp.length(r_vec[i]))
+ omega = dR[i] * alpha
+
+ dQ = wp.quat(omega, 0.0)
+ Q = wp.normalize(Q + 0.5 * Q * dQ)
+
+ axis = wp.vec3()
+ angle = float(0)
+ wp.quat_to_axis_angle(Q, axis, angle)
+ r_vec[i] = axis * angle
+
+
+@fem.integrand
+def tensor_mass_form(s: Sample, sig: Field, tau: Field):
+ """
+ Mass form over tensor space
+ sig : tau
+ """
+ return wp.ddot(sig(s), tau(s))
+
+
+class LineSearchMeritCriterion:
+ # Numeric Optimization, chapter 15.4
+
+ def __init__(self, sim: SoftbodySim):
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ delta_u, dS, dR, dLambda = delta_fields
+
+ c_k = rhs[3]
+ c_k_normalized = wp.empty_like(c_k)
+
+ wp.launch(
+ self._normalize_c_k,
+ inputs=[c_k, c_k_normalized, sim._stiffness_field.dof_values],
+ dim=c_k.shape,
+ )
+
+ delta_ck = lhs._B @ delta_u
+ sp.bsr_mv(A=lhs._Cs, x=dS, y=delta_ck, alpha=-1.0, beta=1.0)
+
+ if lhs._Cr is not None:
+ sp.bsr_mv(A=lhs._Cr, x=dR, y=delta_ck, alpha=-1.0, beta=1.0)
+
+ m = wp.utils.array_inner(dS, sim._dE_dS.view(dS.dtype)) - wp.utils.array_inner(
+ delta_u, sim._minus_dE_du.view(delta_u.dtype)
+ ) * wp.utils.array_inner(c_k_normalized, delta_ck.view(c_k_normalized.dtype))
+
+ self.m = m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ f_cur = E_cur + C_cur
+ f_ref = E_ref + C_ref
+
+ return f_cur <= f_ref + self.armijo_coeff * alpha * self.m
+
+ @wp.kernel
+ def _normalize_c_k(
+ c_k: wp.array(dtype=Any),
+ c_k_norm: wp.array(dtype=Any),
+ scale: wp.array(dtype=float),
+ ):
+ i = wp.tid()
+ c_k_norm[i] = wp.normalize(c_k[i]) * scale[i]
+
+
+class LineSearchMultiObjCriterion:
+ # Line Search Filter Methods for Nonlinear Programming: Motivation and Global Convergence
+ # 2005, SIAM Journal on Optimization 16(1):1-31
+
+ def __init__(self, sim: SoftbodySim):
+ # constraint decrease
+ E_scale = sim.typical_stiffness / sim.lame_ref[1]
+ self.gamma_theta = 0.75
+ self.gamma_f = 0.1 * E_scale
+
+ # switching rule
+ self.s_theta = 1.5
+ self.s_rho = 2.5 * self.s_theta
+ self.delta = 0.01 * E_scale ** (self.s_theta / self.s_rho)
+
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ delta_u, dS, dR, dLambda = delta_fields
+ m = wp.utils.array_inner(dS, sim._dE_dS.view(dS.dtype)) - wp.utils.array_inner(
+ delta_u, sim._minus_dE_du.view(delta_u.dtype)
+ )
+ self.m = m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ if (
+ self.m < 0.0
+ and (-self.m) ** self.s_rho * alpha > self.delta * C_ref**self.s_theta
+ ):
+ return E_cur <= E_ref + self.armijo_coeff * alpha * self.m
+
+ return C_cur <= (1.0 - self.gamma_theta) * C_ref or (
+ E_cur <= E_ref - self.gamma_f * C_ref
+ )
+
+
+class LineSearchLagrangianArmijoCriterion:
+ # Unconstrained line-search based on Lagrangian
+
+ def __init__(self, sim: SoftbodySim):
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ delta_u, dS, dR, dLambda = delta_fields
+
+ m = wp.utils.array_inner(dS, sim._dE_dS.view(dS.dtype)) - wp.utils.array_inner(
+ delta_u, sim._minus_dE_du.view(delta_u.dtype)
+ )
+
+ c_k = rhs[3]
+ delta_ck = lhs._B @ delta_u
+ sp.bsr_mv(A=lhs._Cs, x=dS, y=delta_ck, alpha=-1.0, beta=1.0)
+ if lhs._Cr is not None:
+ sp.bsr_mv(A=lhs._Cr, x=dR, y=delta_ck, alpha=-1.0, beta=1.0)
+
+ c_m = wp.utils.array_inner(c_k, dLambda.view(c_k.dtype)) + wp.utils.array_inner(
+ delta_ck, sim.constraint_field.dof_values.view(delta_ck.dtype)
+ )
+ self.m = m - c_m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ return E_cur + C_cur <= E_ref + C_ref + self.armijo_coeff * alpha * self.m
+
+
+class MFEM(SoftbodySim):
+ def __init__(self, geo: fem.Geometry, active_cells: wp.array, args):
+ super().__init__(geo, active_cells, args)
+
+ self._make_elasticity_forms()
+ self._init_strain_basis()
+
+ self._lagrangian_constraint_energy = False
+ if self.args.line_search == "merit":
+ self._ls = LineSearchMeritCriterion(self)
+ elif self.args.line_search == "lagrangian":
+ self._lagrangian_constraint_energy = True
+ self._ls = LineSearchLagrangianArmijoCriterion(self)
+ else:
+ self._ls = LineSearchMultiObjCriterion(self)
+
+ # Temp storage for energy cuda graph
+ self._E = wp.empty(3, dtype=wp.float64)
+ self._E_pinned = wp.empty_like(self._E, device="cpu", pinned=True)
+ self._E_graph = None
+
+ def set_strain_basis(self, strain_basis: Optional[fem.BasisSpace]):
+ if strain_basis is None:
+ self._init_strain_basis()
+ else:
+ self._strain_basis = strain_basis
+
+ def _make_elasticity_forms(self):
+ if self.args.neo_hookean:
+ self.elastic_energy_form = MFEM.nh_elasticity_energy_form
+ self.elastic_gradient_form = MFEM.nh_elasticity_gradient_form
+ self.elastic_hessian_form = MFEM.nh_elasticity_hessian_form
+ else:
+ self.elastic_energy_form = MFEM.hooke_elasticity_energy_form
+ self.elastic_gradient_form = MFEM.hooke_elasticity_gradient_form
+ self.elastic_hessian_form = MFEM.hooke_elasticity_hessian_form
+
+ def _init_strain_basis(self):
+ if isinstance(self.geo.reference_cell(), fem.geometry.element.Cube):
+ strain_degree = self.args.degree
+ strain_basis = fem.ElementBasis.LAGRANGE
+ strain_poly = fem.Polynomial.GAUSS_LEGENDRE
+ else:
+ strain_degree = self.args.degree - 1
+ strain_basis = fem.ElementBasis.NONCONFORMING_POLYNOMIAL
+ strain_poly = None
+
+ self._strain_basis = fem.make_polynomial_basis_space(
+ self.geo,
+ degree=strain_degree,
+ discontinuous=True,
+ element_basis=strain_basis,
+ family=strain_poly,
+ )
+
+ def init_strain_spaces(self, constraint_dof_mapper: fem.DofMapper):
+ sym_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dof_mapper=fem.SymmetricTensorMapper(
+ wp.mat33, mapping=fem.SymmetricTensorMapper.Mapping.DB16
+ ),
+ )
+
+ # Function spaces for piecewise-constant per-element rotations and rotation vectors
+ rot_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dtype=wp.vec3,
+ )
+ skew_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dof_mapper=fem.SkewSymmetricTensorMapper(wp.mat33),
+ )
+ constraint_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dof_mapper=constraint_dof_mapper,
+ )
+
+ strain_space_partition = fem.make_space_partition(
+ space_topology=self._strain_basis.topology,
+ geometry_partition=self._geo_partition,
+ with_halo=False,
+ )
+
+ # Defines some fields over our function spaces
+ self.S = sym_space.make_field(
+ space_partition=strain_space_partition
+ ) # Rotated symmetric train
+ self.S.dof_values.fill_(
+ sym_space.dof_mapper.value_to_dof(
+ wp.mat33(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
+ )
+ ) # initialize with identity
+
+ self.R = rot_space.make_field(
+ space_partition=strain_space_partition
+ ) # Rotation
+
+ self.constraint_field = constraint_space.make_field(
+ space_partition=strain_space_partition
+ )
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ domain = self.u_test.domain
+ self.sym_test = fem.make_test(
+ space=sym_space, space_partition=strain_space_partition, domain=domain
+ )
+ self.sym_trial = fem.make_trial(
+ space=sym_space, space_partition=strain_space_partition, domain=domain
+ )
+
+ self.skew_test = fem.make_test(
+ space=skew_space, space_partition=strain_space_partition, domain=domain
+ )
+ self.skew_trial = fem.make_trial(
+ space=skew_space, space_partition=strain_space_partition, domain=domain
+ )
+
+ self.constraint_test = fem.make_test(
+ space=constraint_space, space_partition=strain_space_partition
+ )
+
+ # self.strain_quadrature = fem.RegularQuadrature(self.sym_test.domain, order=2 * sym_space.degree)
+ # self.elasticity_quadrature = fem.RegularQuadrature(self.sym_test.domain, order=2 * sym_space.degree)
+ self.strain_quadrature = fem.NodalQuadrature(
+ self.sym_test.domain, space=sym_space
+ )
+ self.elasticity_quadrature = fem.NodalQuadrature(
+ self.sym_test.domain, space=sym_space
+ )
+
+ self._stiffness_field = fem.make_collocated_function_space(
+ self._strain_basis, dtype=float
+ ).make_field()
+ fem.interpolate(
+ MFEM._typical_stiffness_field,
+ fields={"lame": self.lame_field},
+ dest=self._stiffness_field,
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ # Temp storage so we can use cuda graphs (forward pass only)
+ self._u_rhs = wp.empty_like(self.u_field.dof_values, requires_grad=False)
+ self._f = wp.empty_like(self.S.dof_values, requires_grad=False)
+ self._w = wp.empty_like(self.R.dof_values, requires_grad=False)
+ self._c_k = wp.empty_like(self.constraint_field.dof_values, requires_grad=False)
+ self._rhs_graph = None
+
+ # For line search
+ self._minus_dE_du = wp.empty_like(self._u_rhs)
+ self._dE_dS = wp.empty_like(self._f)
+ self._dE_dR = wp.empty_like(self._w)
+
+ self._schur_work_arrays = None
+
+ def reset_fields(self):
+ super().reset_fields()
+
+ self.S.dof_values.fill_(
+ self.S.space.dof_mapper.value_to_dof(
+ wp.mat33(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
+ )
+ ) # initialize with identity
+ self.R.dof_values.zero_()
+ self.constraint_field.dof_values.zero_()
+
+ def checkpoint_newton_values(self):
+ super().checkpoint_newton_values()
+
+ self._R_cur = wp.clone(self.R.dof_values)
+ self._S_cur = wp.clone(self.S.dof_values)
+ self._lbd_cur = wp.clone(self.constraint_field.dof_values)
+
+ def apply_newton_deltas(self, delta_fields, alpha=1.0):
+ super().apply_newton_deltas(delta_fields, alpha=alpha)
+
+ wp.copy(src=self._S_cur, dest=self.S.dof_values)
+ wp.copy(src=self._lbd_cur, dest=self.constraint_field.dof_values)
+
+ _, dS, dR, dLambda = delta_fields
+
+ array_axpy(x=dS, y=self.S.dof_values, alpha=alpha)
+
+ array_axpy(
+ x=dLambda.view(dtype=self.constraint_field.dof_values.dtype),
+ y=self.constraint_field.dof_values,
+ alpha=alpha,
+ )
+
+ self._apply_rotation_delta(dR, alpha)
+
+ def _apply_rotation_delta(self, dR, alpha):
+ wp.copy(src=self._R_cur, dest=self.R.dof_values)
+ wp.launch(
+ apply_rotation_delta, dim=dR.shape[0], inputs=[self.R.dof_values, dR, alpha]
+ )
+
+ def _evaluate_energy(self, E_u, E_e, c_r, lagrangian=False):
+ super().evaluate_energy(E_u=E_u)
+
+ fem.integrate(
+ self.elastic_energy_form,
+ quadrature=self.elasticity_quadrature,
+ fields={"S": self.S, "lame": self.lame_field},
+ output=E_e,
+ )
+
+ if lagrangian:
+ fem.integrate(
+ self.constraint_form,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u": self.u_field,
+ "sig": self.S,
+ "R": self.R,
+ "tau": self.constraint_field,
+ },
+ kernel_options={"enable_backward": True},
+ output=c_r,
+ )
+ else:
+ c_k = wp.empty_like(self.constraint_field.dof_values, requires_grad=False)
+ self._evaluate_constraint_residual(out=c_k)
+ c_k_norm = wp.empty(shape=c_k.shape, dtype=wp.float64)
+ wp.launch(
+ MFEM.constraint_norm,
+ dim=c_k.shape,
+ inputs=[c_k, c_k_norm, self._stiffness_field.dof_values],
+ )
+ wp.utils.array_sum(c_k_norm, out=c_r)
+
+ def evaluate_energy(self):
+ E_u = self._E[0:1]
+ E_e = self._E[1:2]
+ c_r = self._E[2:3]
+
+ lagrangian = self._lagrangian_constraint_energy
+
+ if (
+ self.args.cuda_graphs
+ and self.__class__._ENERGY_MODULES_LOADED
+ and self._E_graph is None
+ ):
+ try:
+ gc.collect(0)
+ gc.disable()
+ with wp.ScopedCapture(force_module_load=False) as capture:
+ self._evaluate_energy(E_u, E_e, c_r, lagrangian=lagrangian)
+ wp.copy(src=self._E, dest=self._E_pinned)
+ gc.collect(0)
+ gc.enable()
+ self._E_graph = capture.graph
+ except Exception as err:
+ print("Energy graph capture failed", err)
+
+ if self._E_graph is None:
+ self._evaluate_energy(E_u, E_e, c_r, lagrangian=lagrangian)
+ wp.copy(src=self._E, dest=self._E_pinned)
+ self.__class__._ENERGY_MODULES_LOADED = True
+ else:
+ wp.capture_launch(self._E_graph)
+
+ wp.synchronize_stream()
+
+ E = self._E_pinned.numpy()
+ E_tot = E[0] + E[1]
+ c_r = -E[2] if lagrangian else E[2]
+
+ return E_tot, c_r
+
+ def solve_newton_system(self, lhs, rhs):
+ if self.args.fp64:
+ lhs = lhs.cast(wp.float64)
+
+ if self._schur_work_arrays is None:
+ self._schur_work_arrays = sp.bsr_mm_work_arrays()
+ reuse_topology = False
+ else:
+ reuse_topology = True
+
+ return lhs.solve_schur(
+ rhs,
+ max_iters=self.args.cg_iters,
+ tol=self.args.cg_tol,
+ work_arrays=self._schur_work_arrays,
+ reuse_topology=reuse_topology,
+ )
+
+ def newton_rhs(self, tape: wp.Tape = None):
+ with_gradient = tape is not None
+
+ u_rhs = self.constraint_free_rhs(tape=tape)
+
+ if with_gradient:
+ c_k = wp.empty_like(self.constraint_field.dof_values, requires_grad=True)
+ f = wp.empty_like(self.S.dof_values, requires_grad=True)
+ w = wp.empty_like(self.R.dof_values, requires_grad=True)
+
+ with tape:
+ self._assemble_rhs(u_rhs, f, w, c_k)
+ else:
+ wp.copy(src=u_rhs, dest=self._u_rhs)
+ u_rhs = self._u_rhs
+ f = self._f
+ w = self._w
+ c_k = self._c_k
+
+ if (
+ self.args.cuda_graphs
+ and self.__class__._RHS_MODULES_LOADED
+ and self._rhs_graph is None
+ ):
+ try:
+ gc.collect(0)
+ gc.disable()
+ with wp.ScopedCapture(force_module_load=False) as capture:
+ self._assemble_rhs(
+ u_rhs,
+ f,
+ w,
+ c_k,
+ minus_dE_dU=self._minus_dE_du,
+ dE_dS=self._dE_dS,
+ dE_dR=self._dE_dR,
+ )
+ gc.collect(0)
+ gc.enable()
+ self._rhs_graph = capture.graph
+ except Exception as err:
+ print("RHS CAPTURE FAILED", err)
+
+ if self._rhs_graph is None:
+ self._assemble_rhs(
+ u_rhs,
+ f,
+ w,
+ c_k,
+ minus_dE_dU=self._minus_dE_du,
+ dE_dS=self._dE_dS,
+ dE_dR=self._dE_dR,
+ )
+ self.__class__._RHS_MODULES_LOADED = True
+ else:
+ wp.capture_launch(self._rhs_graph)
+
+ # Displacement boundary condition -- Filter u rhs
+ self._filter_forces(u_rhs, tape=tape)
+
+ return u_rhs, f, w, c_k
+
+ def record_adjoint(self, tape):
+ # The forward Newton is finding a root of rhs(q, p) = 0 with q = (u, S, R, lambda)
+ # so drhs/dp = drhs/dq dq/dp + drhs/dp = 0
+ # [- drhs/dq] dq/dp = drhs/dp
+ # lhs dq/dp = drhs/dp
+
+ self.prepare_newton_step(tape)
+ rhs = self.newton_rhs(tape=tape)
+ lhs = self.newton_lhs()
+
+ def solve_backward():
+ adj_res = (
+ self.u_field.dof_values.grad,
+ *(wp.zeros_like(field) for field in rhs[1:-1]),
+ self.constraint_field.dof_values.grad,
+ )
+ delta_du, dS, dR, dLambda = lhs.cast(wp.float64).solve_schur(
+ adj_res, max_iters=self.args.cg_iters
+ )
+
+ u_rhs, f, w_lambda, c_k = rhs
+ wp.copy(src=delta_du, dest=u_rhs.grad)
+ wp.copy(src=dLambda, dest=c_k.grad)
+ array_axpy(dS, f.grad, alpha=-1.0, beta=0.0)
+ array_axpy(dR, w_lambda.grad, alpha=-1.0, beta=0.0)
+
+ tape.record_func(
+ solve_backward,
+ arrays=[
+ self.u_field.dof_values,
+ self.constraint_field.dof_values,
+ *rhs,
+ ],
+ )
+
+ def interpolate_constraint_field(self, strain=False):
+ tau = fem.make_test(
+ self.interpolated_constraint_field.space,
+ space_restriction=self.u_test.space_restriction,
+ )
+
+ if strain:
+ # interpolate strain instead of stress field
+ fem.integrate(
+ tensor_mass_form,
+ quadrature=self.strain_quadrature,
+ fields={"sig": self.S, "tau": tau},
+ output=self.interpolated_constraint_field.dof_values,
+ kernel_options={"enable_backward": True},
+ )
+ else:
+ fem.integrate(
+ tensor_mass_form,
+ quadrature=self.strain_quadrature,
+ fields={"sig": self.constraint_field, "tau": tau},
+ output=self.interpolated_constraint_field.dof_values,
+ kernel_options={"enable_backward": True},
+ )
+
+ # Scale by inverse mass
+
+ if self._mass is None:
+ mass_test = fem.make_test(
+ self._mass_space, space_partition=self.u_test.space_partition
+ )
+ self._mass = fem.integrate(
+ self.mass_form,
+ quadrature=self.strain_quadrature,
+ fields={"p": mass_test},
+ output_dtype=wp.float32,
+ )
+
+ wp.launch(
+ self.scale_interpolated_quantity,
+ dim=self._mass.shape,
+ inputs=[self.interpolated_constraint_field.dof_values, self._mass],
+ )
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ super(MFEM, MFEM).add_parser_arguments(parser)
+
+ parser.add_argument(
+ "--cuda-graphs", action=argparse.BooleanOptionalAction, default=True
+ )
+ parser.add_argument(
+ "--line-search",
+ "-ls",
+ choices=["merit", "mobj", "lagrangian"],
+ default="merit",
+ )
+
+ @fem.integrand
+ def mass_form(s: Sample, p: Field):
+ return p(s)
+
+ @wp.kernel
+ def scale_interpolated_quantity(
+ qtt: wp.array(dtype=wp.mat33), mass: wp.array(dtype=float)
+ ):
+ i = wp.tid()
+ qtt[i] = qtt[i] / mass[i]
+
+ @fem.integrand
+ def _typical_stiffness_field(s: Sample, lame: Field):
+ return wp.min(lame(s))
+
+ @fem.integrand
+ def hooke_elasticity_hessian_form(
+ s: Sample, domain: Domain, S: Field, tau: Field, sig: Field, lame: Field
+ ):
+ return hooke_hessian(S(s), tau(s), sig(s), lame(s))
+
+ @fem.integrand
+ def hooke_elasticity_gradient_form(
+ s: Sample, domain: Domain, tau: Field, S: Field, lame: Field
+ ):
+ return wp.ddot(tau(s), hooke_stress(S(s), lame(s)))
+
+ @fem.integrand
+ def hooke_elasticity_energy_form(s: Sample, domain: Domain, S: Field, lame: Field):
+ return hooke_energy(S(s), lame(s))
+
+ @fem.integrand
+ def nh_elasticity_hessian_form(
+ s: Sample, domain: Domain, S: Field, tau: Field, sig: Field, lame: Field
+ ):
+ return snh_hessian_proj(S(s), tau(s), sig(s), lame(s))
+
+ @fem.integrand
+ def nh_elasticity_gradient_form(
+ s: Sample, domain: Domain, tau: Field, S: Field, lame: Field
+ ):
+ return wp.ddot(tau(s), snh_stress(S(s), lame(s)))
+
+ @fem.integrand
+ def nh_elasticity_energy_form(s: Sample, domain: Domain, S: Field, lame: Field):
+ return snh_energy(S(s), lame(s))
+
+ @wp.kernel
+ def constraint_norm(
+ C: wp.array(dtype=Any),
+ C_norm: wp.array(dtype=wp.float64),
+ scale: wp.array(dtype=wp.float32),
+ ):
+ i = wp.tid()
+ Ci = C[i]
+ C_norm[i] = wp.float64(wp.sqrt(0.5 * wp.dot(Ci, Ci)) * scale[i])
+
+
+class MFEM_RS_F(MFEM):
+ """RS = F variant"""
+
+ _RHS_MODULES_LOADED = False
+ _ENERGY_MODULES_LOADED = False
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces(constraint_dof_mapper=FullTensorMapper())
+
+ self._pen_field = fem.make_collocated_function_space(
+ self._strain_basis,
+ dtype=wp.vec2,
+ ).make_field(space_partition=self.sym_test.space_partition)
+ self._pen_field_restr = fem.make_restriction(
+ self._pen_field, space_restriction=self.sym_test.space_restriction
+ )
+
+ def supports_discontinuities(self):
+ return True
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ if self.has_discontinuities():
+ self._constraint_side_test = fem.make_test(
+ self.constraint_test.space,
+ space_partition=self.constraint_test.space_partition,
+ domain=self.u_side_test.domain,
+ )
+
+ # Displacement gradient matrix
+ self.B = fem.integrate(
+ self.dispgrad_form,
+ fields={"tau": self.constraint_test, "u": self.u_trial},
+ output_dtype=float,
+ quadrature=self.strain_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self.side_quadrature is not None:
+ self.B += fem.integrate(
+ self.dispgrad_side_form,
+ fields={"tau": self._constraint_side_test, "u": self.u_side_trial},
+ output_dtype=float,
+ quadrature=self.side_quadrature,
+ )
+ self.B.nnz_sync()
+
+ # Temp storage for lhs cuda graph
+ self._lhs = None
+ self._lhs_graph = None
+
+ def project_constant_forms(self):
+ super().project_constant_forms()
+
+ self.B_proj = sp.bsr_copy(self.B)
+ sp.bsr_mm(x=self.B, y=self.v_bd_matrix, z=self.B_proj, alpha=-1.0, beta=1.0)
+ self.B_proj.nnz_sync()
+
+ self.Bt_proj = sp.bsr_transposed(self.B_proj)
+ self.Bt_proj.nnz_sync()
+
+ def prepare_newton_step(self, tape: Optional[wp.Tape] = None):
+ # Update penalization (no contribution to derivatives)
+ backward_step = tape is not None
+ fem.interpolate(
+ self.penalization_field,
+ dest=self._pen_field_restr,
+ fields={
+ "S": self.S,
+ "R": self.R,
+ "stress": self.constraint_field,
+ "lame": self.lame_field,
+ },
+ values={
+ "rot_compliance": 0.0 if backward_step else self.args.rot_compliance,
+ "typ_stiff": self.typical_stiffness * self.args.constraint_pen,
+ },
+ )
+
+ def newton_lhs(self):
+ if self.args.cuda_graphs and self._lhs is not None and self._lhs_graph is None:
+ try:
+ gc.collect(0)
+ gc.disable()
+ with wp.ScopedCapture(force_module_load=False) as capture:
+ self._assemble_lhs(self._lhs)
+ gc.collect(0)
+ gc.enable()
+ self._lhs_graph = capture.graph
+ except Exception as err:
+ print("LHS capture failed", err)
+
+ if self._lhs_graph is None:
+ if self._lhs is None:
+ self._lhs = self._assemble_lhs()
+ self._lhs._H_pen = sp.bsr_copy(self._lhs._H)
+ else:
+ self._assemble_lhs(self._lhs)
+ else:
+ wp.capture_launch(self._lhs_graph)
+
+ self._lhs._A = self.A_proj
+ self._lhs._B = self.B_proj
+ self._lhs._Bt = self.Bt_proj
+
+ return self._lhs
+
+ def _assemble_lhs(self, lhs: MFEMSystem = None):
+ W_skew = fem.integrate(
+ MFEM_RS_F.rot_penalization,
+ fields={
+ "tau": self.skew_test,
+ "sig": self.skew_trial,
+ "S": self.S,
+ "pen": self._pen_field,
+ },
+ nodal=True,
+ output=lhs._W if lhs else None,
+ output_dtype=float,
+ )
+
+ # Grad of rotated strain w.r.t R, S
+ CSk = fem.integrate(
+ self.rotated_strain_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "R": self.R, "tau": self.constraint_test},
+ output=lhs._Cs if lhs else None,
+ output_dtype=float,
+ kernel_options={"enable_backward": True},
+ )
+ CRk = fem.integrate(
+ self.incremental_strain_rotation_form,
+ nodal=True,
+ fields={
+ "sig": self.S,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.constraint_test,
+ },
+ output=lhs._Cr if lhs else None,
+ output_dtype=float,
+ kernel_options={"enable_backward": True},
+ )
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={
+ "S": self.S,
+ "sig": self.sym_trial,
+ "tau": self.sym_test,
+ "lame": self.lame_field,
+ },
+ output=lhs._H if lhs else None,
+ output_dtype=float,
+ )
+ H_pen = fem.integrate(
+ MFEM_RS_F.strain_penalization,
+ fields={
+ "tau": self.sym_test,
+ "sig": self.sym_trial,
+ "pen": self._pen_field,
+ },
+ nodal=True,
+ output=lhs._H_pen if lhs else None,
+ output_dtype=float,
+ )
+ fem.utils.array_axpy(x=H_pen.values, y=H.values)
+
+ return MFEMSystem(
+ self.A_proj, H, W_skew, self.B_proj, CSk, CRk, Bt=self.Bt_proj
+ )
+
+ def _evaluate_constraint_residual(self, out):
+ fem.integrate(
+ self.constraint_form,
+ nodal=True,
+ fields={
+ "u": self.u_field,
+ "tau": self.constraint_test,
+ "sig": self.S,
+ "R": self.R,
+ },
+ kernel_options={"enable_backward": True},
+ output=out,
+ )
+
+ if self.side_quadrature is not None:
+ fem.integrate(
+ self.dispgrad_side_form,
+ quadrature=self.side_quadrature,
+ fields={
+ "u": self.u_field.trace(),
+ "tau": self._constraint_side_test,
+ },
+ kernel_options={"enable_backward": True},
+ output=out,
+ add=True,
+ )
+
+ def _assemble_rhs(self, u_rhs, f, w, c_k, minus_dE_dU=None, dE_dS=None, dE_dR=None):
+ if minus_dE_dU:
+ wp.copy(src=u_rhs, dest=minus_dE_dU)
+
+ # Add current stresses
+ fem.integrate(
+ self.dispgrad_form,
+ quadrature=self.strain_quadrature,
+ fields={"u": self.u_test, "tau": self.constraint_field},
+ kernel_options={"enable_backward": True},
+ output=u_rhs,
+ add=True,
+ )
+
+ if self.side_quadrature is not None:
+ fem.integrate(
+ self.dispgrad_side_form,
+ quadrature=self.side_quadrature,
+ fields={
+ "u": self.u_side_test,
+ "tau": self.constraint_field.trace(),
+ },
+ kernel_options={"enable_backward": True},
+ output=u_rhs,
+ add=True,
+ )
+
+ # c_k -- constraint residual (Fk - RS)
+ self._evaluate_constraint_residual(out=c_k)
+
+ # Other primal variables:
+ # Symmetric and skew-symmetric strains
+
+ # Elastic stress + Lagrange multiplier
+ fem.integrate(
+ self.elastic_gradient_form,
+ nodal=True,
+ fields={"S": self.S, "tau": self.sym_test, "lame": self.lame_field},
+ output=f,
+ kernel_options={"enable_backward": True},
+ )
+
+ if dE_dS:
+ wp.copy(src=f, dest=dE_dS)
+
+ fem.integrate(
+ self.strain_penalization_rhs,
+ nodal=True,
+ fields={
+ "sig": self.sym_test,
+ "u": self.u_field,
+ "R": self.R,
+ "S": self.S,
+ "pen": self._pen_field,
+ },
+ output=f,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ fem.integrate(
+ self.rotated_strain_form,
+ nodal=True,
+ fields={
+ "sig": self.sym_test,
+ "R": self.R,
+ "tau": self.constraint_field,
+ },
+ output=f,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ # Rotational stress
+
+ fem.integrate(
+ self.rot_penalization_rhs,
+ nodal=True,
+ fields={
+ "sig": self.skew_test,
+ "u": self.u_field,
+ "R": self.R,
+ "S": self.S,
+ "pen": self._pen_field,
+ },
+ output=w,
+ kernel_options={"enable_backward": True},
+ )
+
+ if dE_dR:
+ dE_dR.zero_()
+
+ fem.integrate(
+ self.incremental_strain_rotation_form,
+ nodal=True,
+ fields={
+ "sig": self.S,
+ "dR": self.skew_test,
+ "R": self.R,
+ "tau": self.constraint_field,
+ },
+ output=w,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ super(MFEM_RS_F, MFEM_RS_F).add_parser_arguments(parser)
+
+ parser.add_argument("--constraint_pen", type=float, default=0.01)
+ parser.add_argument("--rot_compliance", type=float, default=0.1)
+
+ @fem.integrand
+ def constraint_form(
+ domain: Domain, s: Sample, u: Field, tau: Field, R: Field, sig: Field
+ ):
+ C = defgrad(u, s) - rotation_matrix(R(s)) * sig(s)
+ return wp.ddot(C, tau(s))
+
+ @fem.integrand
+ def dispgrad_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau^T
+ """
+ return wp.ddot(tau(s), fem.grad(u, s))
+
+ @fem.integrand
+ def dispgrad_side_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau^T
+ """
+ grad_h = -wp.outer(fem.jump(u, s), fem.normal(domain, s))
+ return wp.ddot(tau(s), grad_h)
+
+ @fem.integrand
+ def rotated_strain_form(
+ s: Sample, domain: Domain, R: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R S : tau^T
+ """
+ return wp.ddot(rotation_matrix(R(s)) * sig(s), tau(s))
+
+ @fem.integrand
+ def incremental_strain_rotation_form(
+ s: Sample, domain: Domain, R: Field, dR: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R dR S : tau^T
+ """
+ return wp.ddot(rotation_matrix(R(s)) * dR(s) * sig(s), tau(s))
+
+ @fem.integrand
+ def rot_penalization_rhs(
+ s: Sample,
+ sig: Field,
+ u: Field,
+ R: Field,
+ S: Field,
+ pen: Field,
+ ):
+ S_s = S(s)
+ R_s = rotation_matrix(R(s))
+ F_s = defgrad(u, s)
+ C_s = F_s - R_s * S_s
+ return -pen(s)[0] * wp.ddot(R_s * sig(s) * S_s, C_s)
+
+ @fem.integrand
+ def rot_penalization(
+ s: Sample,
+ sig: Field,
+ tau: Field,
+ S: Field,
+ pen: Field,
+ ):
+ S_s = S(s)
+ return pen(s)[0] * wp.ddot(sig(s) * S_s, tau(s) * S_s) + pen(s)[1] * wp.ddot(
+ sig(s), tau(s)
+ )
+
+ @fem.integrand
+ def strain_penalization_rhs(
+ s: Sample, sig: Field, u: Field, R: Field, S: Field, pen: Field
+ ):
+ S_s = S(s)
+ R_s = rotation_matrix(R(s))
+ F_s = defgrad(u, s)
+ C_s = F_s - R_s * S_s
+ return -pen(s)[0] * wp.ddot(R_s * sig(s), C_s)
+
+ @fem.integrand
+ def strain_penalization(
+ s: Sample,
+ sig: Field,
+ tau: Field,
+ pen: Field,
+ ):
+ return pen(s)[0] * wp.ddot(sig(s), tau(s))
+
+ @fem.integrand
+ def penalization_field(
+ s: Sample,
+ S: Field,
+ R: Field,
+ stress: Field,
+ lame: Field,
+ rot_compliance: float,
+ typ_stiff: float,
+ ):
+ rot = rotation_matrix(R(s))
+ strain = S(s)
+ sym_stress = wp.transpose(rot) * stress(s)
+
+ skew_stress = sym_stress - wp.transpose(sym_stress)
+ lbd_pen = wp.sqrt(wp.ddot(skew_stress, skew_stress)) * wp.sqrt(
+ wp.ddot(strain, strain)
+ )
+
+ return wp.vec2(typ_stiff, rot_compliance * lbd_pen)
+
+
+class MFEM_sF_S(MFEM):
+ """s(F) = S variant (Trusty SigAsia22)"""
+
+ _RHS_MODULES_LOADED = False
+ _ENERGY_MODULES_LOADED = False
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces(
+ constraint_dof_mapper=fem.SymmetricTensorMapper(
+ dtype=wp.mat33, mapping=fem.SymmetricTensorMapper.Mapping.DB16
+ ),
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.C = fem.integrate(
+ tensor_mass_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "tau": self.constraint_test},
+ output_dtype=float,
+ kernel_options={"enable_backward": True},
+ )
+
+ def _apply_rotation_delta(self, dR, alpha):
+ pass
+
+ def newton_lhs(self):
+ # Grad of rotated strain w.r.t R, S
+
+ Bk_proj = fem.integrate(
+ self.rotated_dispgrad_form,
+ fields={
+ "u_cur": self.u_field,
+ "tau": self.constraint_test,
+ "u": self.u_trial,
+ },
+ output_dtype=float,
+ quadrature=self.strain_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ sp.bsr_mm(x=Bk_proj, y=self.v_bd_matrix, z=Bk_proj, alpha=-1.0, beta=1.0)
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={
+ "S": self.S,
+ "sig": self.sym_trial,
+ "tau": self.sym_test,
+ "lame": self.lame_field,
+ },
+ output_dtype=float,
+ )
+
+ return MFEMSystem(self.A_proj, H, None, Bk_proj, self.C, None)
+
+ def _evaluate_constraint_residual(self, out):
+ fem.integrate(
+ self.constraint_form,
+ nodal=True,
+ fields={
+ "u": self.u_field,
+ "tau": self.constraint_test,
+ "sig": self.S,
+ "R": self.R, # unused
+ },
+ kernel_options={"enable_backward": True},
+ output=out,
+ )
+
+ def _assemble_rhs(self, u_rhs, f, w, c_k, minus_dE_dU=None, dE_dS=None, dE_dR=None):
+ if minus_dE_dU:
+ wp.copy(src=u_rhs, dest=minus_dE_dU)
+
+ # Add current stresses
+ fem.integrate(
+ self.rotated_dispgrad_form,
+ quadrature=self.strain_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "u": self.u_test,
+ "tau": self.constraint_field,
+ },
+ kernel_options={"enable_backward": True},
+ output=u_rhs,
+ add=True,
+ )
+
+ # constraint residual
+ self._evaluate_constraint_residual(out=c_k)
+
+ # Symmetric strain
+
+ # Elastic stress + Lagrange multiplier
+ fem.integrate(
+ self.elastic_gradient_form,
+ nodal=True,
+ fields={"S": self.S, "tau": self.sym_test, "lame": self.lame_field},
+ output=f,
+ kernel_options={"enable_backward": True},
+ )
+
+ if dE_dS:
+ wp.copy(src=f, dest=dE_dS)
+
+ fem.integrate(
+ tensor_mass_form,
+ nodal=True,
+ fields={
+ "sig": self.sym_test,
+ "tau": self.constraint_field,
+ },
+ output=f,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ @fem.integrand
+ def constraint_form(
+ domain: Domain, s: Sample, u: Field, tau: Field, sig: Field, R: Field
+ ):
+ C = symmetric_strain(defgrad(u, s)) - sig(s)
+ return wp.ddot(C, tau(s))
+
+ @fem.integrand
+ def rotated_dispgrad_form(
+ domain: Domain,
+ s: Sample,
+ u_cur: Field,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Rotated deformation gradient form
+ dS : tau
+ """
+ F = defgrad(u_cur, s)
+ dF = fem.grad(u, s)
+ return wp.ddot(symmetric_strain_delta(F, dF), tau(s))
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_adaptive.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_adaptive.py
new file mode 100644
index 0000000000000000000000000000000000000000..6191e1b11a129227ccf743b45e81c731b1c95e04
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/mfem_adaptive.py
@@ -0,0 +1,129 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+import argparse
+
+import numpy as np
+import warp as wp
+import warp.fem as fem
+import warp.examples.fem.utils as fem_example_utils
+
+from fem_examples.mfem.mfem_3d import MFEM_RS_F
+from fem_examples.mfem.softbody_sim import run_softbody_sim
+
+import polyscope as ps
+
+
+@fem.integrand
+def boundary_projector_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = fem.normal(domain, s)
+ pos = domain(s)
+ clamped = float(0.0)
+
+ # Single clamped point
+ # if s.qp_index < 10:
+ # clamped = 1.0
+
+ # clamped vertical sides
+ # clamped = wp.abs(nor[0])
+
+ # clamped right sides
+ clamped = wp.where(pos[0] < 1.0, 0.0, 1.0)
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@fem.integrand
+def refinement_field(s: fem.Sample, stress: fem.Field, max_p: float):
+ p = wp.abs(wp.trace(stress(s))) / max_p
+ return wp.max(0.0, 1.0 - p)
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+
+ # sim_class = ClassicFEM
+ sim_class = MFEM_RS_F
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument("--displacement", type=float, default=0.0)
+ sim_class.add_parser_arguments(parser)
+ args = parser.parse_args()
+
+ args.grid = True
+
+ vol1 = fem_example_utils.gen_volume(
+ # res=wp.vec3i(args.resolution), bounds_lo=wp.vec3(0.0, 0.5, 0.875)
+ res=wp.vec3i(args.resolution),
+ bounds_lo=wp.vec3(0.0, 0.75, 0.75),
+ )
+ coarse_grid = fem.Nanogrid(vol1)
+
+ ref_field = fem.make_polynomial_space(coarse_grid, degree=3).make_field()
+ ref_field.dof_values.fill_(1.0)
+
+ def frame_callback(displaced_pos):
+ sim.interpolate_constraint_field(strain=False)
+
+ vol_mesh = ps.get_volume_mesh("volume mesh")
+ stress = sim.interpolated_constraint_field.dof_values.numpy()
+ stress_n = np.abs(np.trace(stress, axis1=1, axis2=2))
+ max_stress = np.max(stress_n)
+ print(max_stress)
+ print(stress_n.shape)
+ vol_mesh.add_scalar_quantity("stress", stress_n, enabled=True)
+
+ for k in range(4):
+ geo = fem.adaptive_nanogrid_from_field(
+ coarse_grid=vol1, level_count=4, refinement_field=ref_field, grading="face"
+ )
+
+ sim = sim_class(geo, active_cells=None, args=args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ sim.set_boundary_condition(
+ boundary_projector_form=boundary_projector_form,
+ )
+
+ run_softbody_sim(sim, frame_callback=frame_callback)
+
+ sim.interpolate_constraint_field(strain=False)
+ stress_field = fem.NonconformingField(
+ fem.Cells(coarse_grid),
+ sim.interpolated_constraint_field,
+ background=wp.mat33f(100.0),
+ )
+ fem.interpolate(
+ refinement_field,
+ dest=ref_field,
+ fields={"stress": stress_field},
+ values={"max_p": 50.0},
+ )
+
+ # pc = ps.register_point_cloud("ref", ref_field.space.node_positions().numpy())
+ # c.add_scalar_quantity("ref", ref_field.dof_values.numpy(), enabled=True)
+ # ps.show()
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/prescribed.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/prescribed.py
new file mode 100644
index 0000000000000000000000000000000000000000..57d53499de945b289047c7c48e05efb768b8d6d4
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/prescribed.py
@@ -0,0 +1,246 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Dict, Any
+
+import warp as wp
+import warp.fem as fem
+
+from .softbody_sim import SoftbodySim
+
+
+class QPBasedImplicitField(fem.field.GeometryField):
+ """Same as fem.ImplicitField, but passes QP index instead of grid-space position"""
+
+ def __init__(
+ self,
+ domain: fem.GeometryDomain,
+ func: wp.Function,
+ values: Optional[Dict[str, Any]] = None,
+ degree=0,
+ ):
+ self.domain = domain
+ self._degree = degree
+
+ if not isinstance(func, wp.Function):
+ raise ValueError(
+ "Implicit field function must be a warp Function (decorated with `wp.func`)"
+ )
+
+ self._func = func
+
+ argspec = fem.integrand(func.func).argspec
+ arg_types = argspec.annotations
+
+ qp_arg_type = arg_types.pop(argspec.args[0]) if arg_types else None
+ if not qp_arg_type or not wp.types.types_equal(
+ qp_arg_type,
+ int,
+ match_generic=True,
+ ):
+ raise ValueError(
+ f"QP-based Implicit field function '{func.func.__name__}' must accept an index as its first argument"
+ )
+
+ pos_arg_type = arg_types.pop(argspec.args[1]) if arg_types else None
+ if not pos_arg_type or not wp.types.types_equal(
+ pos_arg_type,
+ wp.vec3,
+ match_generic=True,
+ ):
+ raise ValueError(
+ f"QP-based Implicit field function '{func.func.__name__}' must accept a position as its second argument"
+ )
+
+ self.EvalArg = fem.cache.get_argument_struct(arg_types)
+ self.values = values
+
+ self.ElementEvalArg = self._make_element_eval_arg()
+ self.eval_degree = self._make_eval_degree()
+
+ self.eval_inner = self._make_eval_func(func)
+ self.eval_outer = self.eval_inner
+
+ @property
+ def values(self):
+ return self._func_arg
+
+ @values.setter
+ def values(self, v):
+ self._func_arg = fem.cache.populate_argument_struct(
+ self.EvalArg, v, self._func.func.__name__
+ )
+
+ @property
+ def geometry(self):
+ return self.domain.geometry
+
+ @property
+ def element_kind(self):
+ return self.domain.element_kind
+
+ def eval_arg_value(self, device):
+ return self._func_arg
+
+ @property
+ def degree(self) -> int:
+ return self._degree
+
+ @property
+ def name(self) -> str:
+ return f"Implicit_{self.domain.name}_{self.degree}_{self.EvalArg.key}"
+
+ def _make_eval_func(self, func):
+ if func is None:
+ return None
+
+ @fem.cache.dynamic_func(
+ suffix=f"{self.name}_{func.key}",
+ code_transformers=[
+ fem.cache.ExpandStarredArgumentStruct({"args.eval_arg": self.EvalArg})
+ ],
+ )
+ def eval_inner(args: self.ElementEvalArg, s: fem.Sample):
+ return func(
+ s.qp_index,
+ self.domain.element_position(args.elt_arg, s),
+ *args.eval_arg,
+ )
+
+ return eval_inner
+
+ def _make_element_eval_arg(self):
+ @fem.cache.dynamic_struct(suffix=self.name)
+ class ElementEvalArg:
+ elt_arg: self.domain.ElementArg
+ eval_arg: self.EvalArg
+
+ return ElementEvalArg
+
+ def _make_eval_degree(self):
+ ORDER = wp.constant(self._degree)
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def degree(args: self.ElementEvalArg):
+ return ORDER
+
+ return degree
+
+
+class PrescribedMotion:
+ def __init__(self, sim: SoftbodySim, quadrature: fem.PicQuadrature):
+ self.sim = sim
+ self.set_quadrature(quadrature)
+ self._prescribed_pos_field = None
+ self._prescribed_pos_weight_field = None
+
+ def set_quadrature(self, quadrature: fem.PicQuadrature):
+ self.quadrature = quadrature
+
+ def set_prescribed_positions(
+ self, pos_field: fem.field.GeometryField, weight_field: fem.field.GeometryField
+ ):
+ # for driving objects kinematically
+ self._prescribed_pos_field = pos_field
+ self._prescribed_pos_weight_field = weight_field
+
+ def add_energy(self, E: float):
+ if self._prescribed_pos_field:
+ E += fem.integrate(
+ prescribed_position_energy_form,
+ quadrature=self.quadrature,
+ fields={
+ "u_cur": self.sim.u_field,
+ "stiffness": self._prescribed_pos_weight_field,
+ "target": self._prescribed_pos_field,
+ },
+ )
+
+ return E
+
+ def add_hessian(self, lhs: wp.array):
+ if self._prescribed_pos_field:
+ z = fem.integrate(
+ prescribed_position_lhs_form,
+ quadrature=self.quadrature,
+ fields={
+ "u": self.sim.u_trial,
+ "v": self.sim.u_test,
+ "stiffness": self._prescribed_pos_weight_field,
+ },
+ output_dtype=float,
+ )
+ lhs += z
+
+ return lhs
+
+ def add_forces(self, rhs: wp.array):
+ if self._prescribed_pos_field:
+ fem.integrate(
+ prescribed_position_rhs_form,
+ quadrature=self.quadrature,
+ fields={
+ "u_cur": self.sim.u_field,
+ "v": self.sim.u_test,
+ "stiffness": self._prescribed_pos_weight_field,
+ "target": self._prescribed_pos_field,
+ },
+ output=rhs,
+ add=True,
+ )
+
+ return rhs
+
+
+@fem.integrand
+def prescribed_position_lhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+ stiffness: fem.Field,
+):
+ u_displ = u(s)
+ v_displ = v(s)
+
+ return stiffness(s) * wp.dot(u_displ, v_displ)
+
+
+@fem.integrand
+def prescribed_position_rhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ v: fem.Field,
+ stiffness: fem.Field,
+ target: fem.Field,
+):
+ pos = u_cur(s) + domain(s)
+ v_displ = v(s)
+ target_pos = target(s)
+ return stiffness(s) * wp.dot(target_pos - pos, v_displ)
+
+
+@fem.integrand
+def prescribed_position_energy_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ stiffness: fem.Field,
+ target: fem.Field,
+):
+ pos = u_cur(s) + domain(s)
+ target_pos = target(s)
+ return 0.5 * stiffness(s) * wp.length_sq(pos - target_pos)
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/softbody_sim.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/softbody_sim.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6c1951b001e6eb9e726a66b680d4975cfd9902f
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/softbody_sim.py
@@ -0,0 +1,1443 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional
+import argparse
+import numpy as np
+
+import warp as wp
+import warp.sparse as sp
+import warp.fem as fem
+
+from warp.fem import Domain, Sample, Field
+from warp.fem.utils import array_axpy
+from warp.optim.linear import LinearOperator
+
+from warp.examples.fem.utils import bsr_cg
+import potpourri3d as pp3d
+
+from fem_examples.mfem.elastic_models import (
+ symmetric_strain,
+ symmetric_strain_delta,
+ snh_energy,
+ snh_stress,
+ snh_hessian_proj_analytic,
+ nh_energy,
+ nh_stress,
+ nh_hessian_proj_analytic,
+ hooke_energy,
+ hooke_stress,
+ hooke_hessian,
+)
+from fem_examples.mfem.linalg import diff_bsr_mv
+
+wp.set_module_options({"enable_backward": False})
+wp.set_module_options({"max_unroll": 4})
+wp.set_module_options({"fast_math": True})
+
+import torch
+import warp.sparse as wps
+
+
+def _wp_bsr_to_torch_bsr(mat: wps.BsrMatrix): # pragma: no cover
+ r"""Converts a sparse warp BSR matrix (or CSR matrix) to a sparse torch matrix.
+
+ Args:
+ mat (wps.BsrMatrix): A sparse warp BSR matrix of shape :math:`(m, n)` with block shape :math:`(b, b)`.
+
+ Returns:
+ torch.sparse_bsr_tensor: A sparse torch matrix of shape :math:`(m, n)` with block shape :math:`(b, b)`.
+ """
+ mat.nnz_sync()
+
+ if mat.block_shape == (1, 1):
+ ctor = torch.sparse_csr_tensor
+ values = wp.to_torch(mat.values[: mat.nnz]).flatten()
+ else:
+ ctor = torch.sparse_bsr_tensor
+ values = wp.to_torch(mat.values[: mat.nnz])
+ torch_weights = ctor(
+ crow_indices=wp.to_torch(mat.offsets[: mat.nrow + 1]),
+ col_indices=wp.to_torch(mat.columns[: mat.nnz]),
+ # values=wp.to_torch(mat.values[: mat.nnz]),
+ values=values,
+ size=mat.shape,
+ )
+ return torch_weights
+
+
+@fem.integrand
+def defgrad(u: fem.Field, s: fem.Sample):
+ return fem.grad(u, s) + wp.identity(n=3, dtype=float)
+
+
+@fem.integrand
+def inertia_form(s: Sample, domain: Domain, u: Field, v: Field, rho: float, dt: float):
+ """"""
+
+ u_rhs = rho * u(s) / (dt * dt)
+ return wp.dot(u_rhs, v(s))
+
+
+@fem.integrand
+def dg_penalty_form(s: Sample, domain: Domain, u: Field, v: Field, k: float):
+ ju = fem.jump(u, s)
+ jv = fem.jump(v, s)
+
+ return wp.dot(ju, jv) * k * fem.measure_ratio(domain, s)
+
+
+@fem.integrand
+def displacement_rhs_form(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ u_prev: Field,
+ v: Field,
+ rho: float,
+ gravity: wp.vec3,
+ dt: float,
+):
+ """ + """
+ f = (
+ inertia_form(s, domain, u_prev, v, rho, dt)
+ - inertia_form(s, domain, u, v, rho, dt)
+ + rho * wp.dot(gravity, v(s))
+ )
+
+ return f
+
+
+@wp.struct
+class VolumetricForces:
+ count: int
+ centers: wp.array(dtype=wp.vec3)
+ radii: wp.array(dtype=float)
+ forces: wp.array(dtype=wp.vec3)
+ tot_weight: wp.array(dtype=float)
+
+
+@wp.func
+def force_weight(x: wp.vec3, forces: VolumetricForces, force_index: int):
+ r = wp.min(
+ wp.length(x - forces.centers[force_index])
+ / (forces.radii[force_index] + 1.0e-7),
+ 1.0,
+ )
+ r2 = r * r
+ return 2.0 * r * r2 - 3.0 * r2 + 1.0 # cubic spline
+
+
+@fem.integrand
+def force_weight_form(
+ s: Sample, domain: Domain, forces: VolumetricForces, force_index: int
+):
+ return force_weight(domain(s), forces, force_index)
+
+
+@fem.integrand
+def force_action(x: wp.vec3, forces: VolumetricForces, force_index: int, vec: wp.vec3):
+ # action of a force over a vector
+ return wp.where(
+ forces.tot_weight[force_index] >= 1.0e-6,
+ wp.dot(forces.forces[force_index], vec)
+ * force_weight(x, forces, force_index)
+ / forces.tot_weight[force_index],
+ 0.0,
+ )
+
+
+@fem.integrand
+def external_forces_form(s: Sample, domain: Domain, v: Field, forces: VolumetricForces):
+ f = float(0.0)
+ x = domain(s)
+ for fi in range(forces.count):
+ f += force_action(x, forces, fi, v(s))
+ return f
+
+
+@fem.integrand
+def kinetic_potential_energy(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+ rho: float,
+ dt: float,
+ gravity: wp.vec3,
+ forces: VolumetricForces,
+):
+ du = u(s)
+ dv = v(s)
+
+ E = rho * (0.5 * wp.dot(du - dv, du - dv) / (dt * dt) - wp.dot(du, gravity))
+ E -= external_forces_form(s, domain, u, forces)
+
+ return E
+
+
+@fem.integrand
+def geo_def_grad_field(s: Sample, domain: Domain):
+ return fem.deformation_gradient(domain, s)
+
+
+@wp.kernel(enable_backward=True)
+def scale_lame(
+ lame_out: wp.array(dtype=wp.vec2),
+ lame_ref: wp.vec2,
+ scale: wp.array(dtype=float),
+):
+ i = wp.tid()
+ lame_out[i] = lame_ref * scale[i]
+
+
+running = False
+
+
+class LineSearchNaiveCriterion:
+ def __init__(self, sim):
+ self.penalty = sim.args.young_modulus
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ pass
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ f_cur = E_cur + self.penalty * C_cur
+ f_ref = E_ref + self.penalty * C_ref
+ return f_cur <= f_ref
+
+
+class LineSearchUnconstrainedArmijoCriterion:
+ def __init__(self, sim):
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ (delta_u,) = delta_fields
+
+ m = -wp.utils.array_inner(delta_u, sim._minus_dE_du.view(delta_u.dtype))
+ self.m = m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ return E_cur <= E_ref + self.armijo_coeff * alpha * self.m
+
+
+class SoftbodySim:
+ def __init__(self, geo: fem.Geometry, active_cells: Optional[wp.array], args):
+ self.args = args
+
+ self.geo = geo
+
+ if active_cells is None:
+ self._geo_partition = fem.geometry.WholeGeometryPartition(geo)
+ else:
+ self._geo_partition = fem.ExplicitGeometryPartition(
+ geo, cell_mask=active_cells
+ )
+ if self._geo_partition.cell_count() == geo.cell_count():
+ self._geo_partition = fem.geometry.WholeGeometryPartition(geo)
+
+ if isinstance(self._geo_partition, fem.ExplicitGeometryPartition):
+ self.cells = self._geo_partition._cells
+ else:
+ self.cells = None
+
+ if self.has_discontinuities() and not self.supports_discontinuities():
+ raise TypeError(
+ f"Simulator of type {type(self)} does not support discontinuities"
+ )
+
+ if self.args.matrix_free and not self.supports_matrix_free():
+ raise TypeError(
+ f"Simulator of type {type(self)} does not support matrix-free solves"
+ )
+
+ if not self.args.quiet:
+ print(f"Active cells: {self._geo_partition.cell_count()}")
+
+ self.dt = args.dt
+
+ self.up_axis = np.zeros(3)
+ self.up_axis[self.args.up_axis] = 1.0
+
+ self.gravity = -self.args.gravity * self.up_axis
+
+ self.forces = VolumetricForces()
+ self.forces.count = 0
+ self.forces.forces = wp.zeros(shape=(0,), dtype=wp.vec3)
+ self.forces.radii = wp.zeros(shape=(0,), dtype=float)
+ self.forces.centers = wp.zeros(shape=(0,), dtype=wp.vec3)
+ self.forces.tot_weight = wp.zeros(shape=(0,), dtype=float)
+
+ young = args.young_modulus
+ poisson = args.poisson_ratio
+ self.lame_ref = wp.vec2(
+ young / (1.0 + poisson) * np.array([poisson / (1.0 - 2.0 * poisson), 0.5])
+ )
+ print(f"young: {young}")
+ print(f"poisson: {poisson}")
+ print(f"lame_ref: {np.array(self.lame_ref)}")
+
+ self.typical_length = 1.0
+ self.typical_stiffness = max(
+ args.density * args.gravity * self.typical_length,
+ min(
+ args.young_modulus, # handle no-gravity, quasistatic case
+ args.density
+ * self.typical_length**2
+ / (args.dt**2), # handle no-gravity, dynamic case
+ ),
+ )
+
+ self._ls = LineSearchNaiveCriterion(self)
+ self._init_displacement_basis()
+
+ self._collision_projector_form = None
+ self._penalty_lhs_form = None
+ self._penalty_rhs_form = None
+
+ self.log = None
+
+ def has_discontinuities(self):
+ return isinstance(self.geo, fem.AdaptiveNanogrid)
+
+ def supports_discontinuities(self):
+ return False
+
+ def supports_matrix_free(self):
+ return False
+
+ def _init_displacement_basis(self):
+ element_basis = (
+ fem.ElementBasis.SERENDIPITY
+ if self.args.serendipity
+ else fem.ElementBasis.LAGRANGE
+ )
+ self._vel_basis = fem.make_polynomial_basis_space(
+ self.geo,
+ degree=self.args.degree,
+ element_basis=element_basis,
+ discontinuous=self.args.discontinuous,
+ )
+
+ def set_displacement_basis(self, basis: fem.BasisSpace = None):
+ if basis is None:
+ self._init_displacement_basis()
+ else:
+ self._vel_basis = basis
+
+ def init_displacement_space(self, side_subdomain: fem.Domain):
+ args = self.args
+
+ u_space = fem.make_collocated_function_space(self._vel_basis, dtype=wp.vec3)
+ u_space_partition = fem.make_space_partition(
+ space_topology=self._vel_basis.topology,
+ geometry_partition=self._geo_partition,
+ with_halo=False,
+ )
+
+ # Defines some fields over our function spaces
+ self.u_field = u_space.make_field(
+ space_partition=u_space_partition
+ ) # displacement
+ self.du_field = u_space.make_field(
+ space_partition=u_space_partition
+ ) # displacement delta
+ self.du_prev = u_space.make_field(
+ space_partition=u_space_partition
+ ) # displacement delta
+ self.force_field = u_space.make_field(
+ space_partition=u_space_partition
+ ) # total force field -- for collision filtering
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ self.u_trial = fem.make_trial(space=u_space, space_partition=u_space_partition)
+ self.u_test = fem.make_test(space=u_space, space_partition=u_space_partition)
+
+ self.vel_quadrature = fem.RegularQuadrature(
+ self.u_test.domain, order=2 * args.degree
+ )
+
+ if self.has_discontinuities():
+ if side_subdomain is None:
+ sides = fem.Sides(self.geo)
+ else:
+ sides = side_subdomain
+
+ self.u_side_trial = fem.make_trial(
+ space=u_space, space_partition=u_space_partition, domain=sides
+ )
+ self.u_side_test = fem.make_test(
+ space=u_space, space_partition=u_space_partition, domain=sides
+ )
+
+ self.side_quadrature = fem.RegularQuadrature(
+ self.u_side_test.domain, order=2 * args.degree
+ )
+ else:
+ self.side_quadrature = None
+
+ # Create material parameters space with same basis as deformation field
+ lame_space = fem.make_polynomial_space(self.geo, dtype=wp.vec2)
+
+ self.lame_field = lame_space.make_field()
+ self.lame_field.dof_values.fill_(self.lame_ref)
+
+ # For interpolating the stress/strain fields back to velocity space
+ interpolated_constraint_space = fem.make_collocated_function_space(
+ self._vel_basis, dtype=wp.mat33
+ )
+ self.interpolated_constraint_field = interpolated_constraint_space.make_field(
+ space_partition=u_space_partition
+ )
+
+ self._mass_space = fem.make_collocated_function_space(
+ self._vel_basis, dtype=float
+ )
+ self._mass = None
+
+ def set_boundary_condition(
+ self,
+ boundary_projector_form,
+ boundary_projector_args={},
+ boundary_displacement_form=None,
+ boundary_displacement_args={},
+ ):
+ u_space = self.u_field.space
+
+ # Displacement boundary conditions
+ boundary = fem.BoundarySides(self._geo_partition)
+
+ u_bd_test = fem.make_test(
+ space=u_space,
+ space_partition=self.u_test.space_partition,
+ domain=boundary,
+ )
+ u_bd_trial = fem.make_trial(
+ space=u_space,
+ space_partition=self.u_test.space_partition,
+ domain=boundary,
+ )
+ self.v_bd_rhs = None
+ if boundary_displacement_form is not None:
+ self.v_bd_rhs = fem.integrate(
+ boundary_displacement_form,
+ fields={"v": u_bd_test},
+ values=boundary_displacement_args,
+ nodal=True,
+ output_dtype=wp.vec3f,
+ )
+ if boundary_projector_form is not None:
+ self.v_bd_matrix = fem.integrate(
+ boundary_projector_form,
+ fields={"u": u_bd_trial, "v": u_bd_test},
+ values=boundary_projector_args,
+ nodal=True,
+ output_dtype=float,
+ )
+ else:
+ self.v_bd_matrix = sp.bsr_zeros(
+ self.u_trial.space_partition.node_count(),
+ self.u_trial.space_partition.node_count(),
+ block_type=wp.mat33,
+ )
+ fem.normalize_dirichlet_projector(self.v_bd_matrix, self.v_bd_rhs)
+
+ def set_fixed_points_condition(
+ self,
+ fixed_points_projector_form,
+ fixed_point_projector_args={},
+ ):
+ self.v_bd_rhs = None
+ self.v_bd_matrix = fem.integrate(
+ fixed_points_projector_form,
+ fields={"u": self.u_trial, "v": self.u_test, "u_cur": self.u_field},
+ values=fixed_point_projector_args,
+ nodal=True,
+ output_dtype=float,
+ )
+
+ self.v_bd_rhs = None
+ fem.normalize_dirichlet_projector(self.v_bd_matrix, self.v_bd_rhs)
+
+ def set_fixed_points_displacement(
+ self,
+ fixed_points_displacement_field=None,
+ fixed_points_displacement_args={},
+ ):
+ bd_field = self.u_test.space.make_field(
+ space_partition=self.u_test.space_partition
+ )
+ fem.interpolate(
+ fixed_points_displacement_field,
+ dest=bd_field,
+ fields={"u_cur": self.u_field},
+ values=fixed_points_displacement_args,
+ )
+
+ self.v_bd_rhs = bd_field.dof_values
+
+ def init_constant_forms(self):
+ args = self.args
+
+ self.update_force_weight()
+
+ if args.matrix_free:
+ self.A = None
+ return
+
+ if self.args.lumped_mass:
+ self.A = fem.integrate(
+ inertia_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt},
+ output_dtype=float,
+ nodal=True,
+ )
+ else:
+ self.A = fem.integrate(
+ inertia_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt},
+ output_dtype=float,
+ quadrature=self.vel_quadrature,
+ )
+
+ if self.side_quadrature is not None:
+ self.A += fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_side_trial, "v": self.u_side_test},
+ values={"k": self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output_dtype=float,
+ )
+
+ if self._penalty_lhs_form:
+ self.A += fem.integrate(
+ self._penalty_lhs_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ output_dtype=float,
+ quadrature=self.vel_quadrature,
+ )
+
+ self.A.nnz_sync()
+
+ def project_constant_forms(self):
+ if self.A is None:
+ return
+
+ self.A_proj = sp.bsr_copy(self.A)
+ fem.dirichlet.project_system_matrix(self.A_proj, self.v_bd_matrix)
+ self.A_proj.nnz_sync()
+
+ def update_force_weight(self):
+ self.forces.tot_weight = wp.empty(shape=self.forces.count, dtype=wp.float32)
+ for fi in range(self.forces.count):
+ wi = self.forces.tot_weight[fi : fi + 1]
+ fem.integrate(
+ force_weight_form,
+ quadrature=self.vel_quadrature,
+ values={
+ "force_index": fi,
+ "forces": self.forces,
+ },
+ output=wi,
+ accumulate_dtype=wp.float32,
+ )
+
+ def constraint_free_rhs(self, dt=None, with_external_forces=True, tape=None):
+ args = self.args
+
+ gravity = self.gravity if with_external_forces else wp.vec3(0.0)
+
+ # Quasi-quasistatic: normal dt in lhs (trust region), large dt in rhs (quasistatic)
+
+ with_gradient = tape is not None
+ rhs_tape = wp.Tape() if tape is None else tape
+ rhs = wp.zeros(
+ dtype=wp.vec3,
+ requires_grad=with_gradient,
+ shape=self.u_test.space_partition.node_count(),
+ )
+
+ with rhs_tape:
+ fem.integrate(
+ displacement_rhs_form,
+ fields={"u": self.du_field, "u_prev": self.du_prev, "v": self.u_test},
+ values={"rho": args.density, "dt": self._step_dt(), "gravity": gravity},
+ output=rhs,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self.side_quadrature is not None:
+ fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_side_test},
+ values={"k": -self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output=rhs,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self._penalty_rhs_form:
+ fem.integrate(
+ self._penalty_rhs_form,
+ fields={"u": self.u_field, "v": self.u_test},
+ output=rhs,
+ add=True,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if with_external_forces and self.forces.count > 0:
+ # NOT differentiating with respect to external forces
+ # Those are assumed to not depend on the geometry
+ fem.integrate(
+ external_forces_form,
+ fields={"v": self.u_test},
+ values={
+ "forces": self.forces,
+ },
+ output_dtype=wp.vec3,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": False},
+ output=rhs,
+ add=True,
+ )
+
+ return rhs
+
+ def run_frame(self):
+ (self.du_field, self.du_prev) = (self.du_prev, self.du_field)
+
+ if self.args.quasi_quasistatic:
+ self.du_prev.dof_values.zero_()
+
+ self.compute_initial_guess()
+
+ tol = self.args.newton_tol**2
+
+ E_cur, C_cur = self.evaluate_energy()
+ cumulative_time = 0.0
+
+ if not self.args.quiet:
+ print(f"Newton initial guess: E={E_cur}, Cr={C_cur}")
+ if self.log:
+ mean_displ = np.mean(
+ np.linalg.norm(self.du_field.dof_values.numpy(), axis=1)
+ )
+ print(
+ "\t".join(
+ str(x)
+ for x in (0, E_cur, C_cur, mean_displ, 0.0, 0.0, cumulative_time)
+ ),
+ file=self.log,
+ )
+
+ for k in range(self.args.n_newton):
+ with wp.ScopedTimer(f"Iter {k}", print=False) as timer:
+ E_ref, C_ref = E_cur, C_cur
+ self.checkpoint_newton_values()
+
+ self.prepare_newton_step()
+ rhs = self.newton_rhs()
+ lhs = self.newton_lhs()
+ delta_fields = self.solve_newton_system(lhs, rhs)
+
+ self.apply_newton_deltas(delta_fields)
+ E_cur, C_cur = self.evaluate_energy()
+
+ ddu = delta_fields[0]
+ step_size = wp.utils.array_inner(ddu, ddu) / (1 + ddu.shape[0])
+
+ # linear model
+ self._ls.build_linear_model(self, lhs, rhs, delta_fields)
+
+ # Line search
+ alpha = 1.0
+ for j in range(self.args.n_backtrack):
+ if self._ls.accept(alpha, E_cur, C_cur, E_ref, C_ref):
+ break
+
+ alpha = 0.5 * alpha
+ self.apply_newton_deltas(delta_fields, alpha=alpha)
+ E_cur, C_cur = self.evaluate_energy()
+
+ if not self.args.quiet:
+ print(
+ f"Newton iter {k}: E={E_cur}, Cr={C_cur}, step size {np.sqrt(step_size)}, alpha={alpha}"
+ )
+
+ cumulative_time += timer.elapsed
+ if self.log:
+ mean_displ = np.mean(
+ np.linalg.norm(self.du_field.dof_values.numpy(), axis=1)
+ )
+ print(
+ "\t".join(
+ str(x)
+ for x in (
+ k + 1,
+ E_cur,
+ C_cur,
+ mean_displ,
+ step_size,
+ alpha,
+ cumulative_time,
+ )
+ ),
+ file=self.log,
+ )
+
+ if step_size < tol:
+ break
+
+ def prepare_newton_step(self, tape=None):
+ pass
+
+ def checkpoint_newton_values(self):
+ self._u_cur = wp.clone(self.u_field.dof_values)
+ self._du_cur = wp.clone(self.du_field.dof_values)
+
+ def apply_newton_deltas(self, delta_fields, alpha=1.0):
+ # Restore checkpoint
+ wp.copy(src=self._u_cur, dest=self.u_field.dof_values)
+ wp.copy(src=self._du_cur, dest=self.du_field.dof_values)
+
+ # Add to total displacement
+ if alpha == 0.0:
+ return
+
+ delta_du = delta_fields[0]
+ array_axpy(x=delta_du, y=self.u_field.dof_values, alpha=alpha)
+ array_axpy(x=delta_du, y=self.du_field.dof_values, alpha=alpha)
+
+ def _step_dt(self):
+ # In fake quasistatic mode, use a large timestep for the rhs computation
+ # Note that self.dt is still use to compute lhs (inertia matrix)
+ return 1.0e6 if self.args.quasi_quasistatic else self.dt
+
+ def evaluate_energy(self, E_u=None, cr=None):
+ E_u = fem.integrate(
+ kinetic_potential_energy,
+ quadrature=self.vel_quadrature,
+ fields={"u": self.du_field, "v": self.du_prev},
+ values={
+ "rho": self.args.density,
+ "dt": self._step_dt(),
+ "gravity": self.gravity,
+ "forces": self.forces,
+ },
+ output=E_u,
+ )
+
+ if self.side_quadrature is not None:
+ if wp.types.is_array(E_u):
+ Eu_dg = wp.empty_like(E_u)
+ else:
+ Eu_dg = None
+
+ Eu_dg = fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_field.trace()},
+ values={"k": self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output=Eu_dg,
+ )
+
+ if wp.types.is_array(E_u):
+ fem.utils.array_axpy(y=E_u, x=Eu_dg)
+ else:
+ E_u += Eu_dg
+
+ return E_u, 0.0
+
+ def _filter_forces(self, u_rhs, tape):
+ if self._collision_projector_form is not None:
+ # update collision projector, if required
+ self.force_field.dof_values = u_rhs
+
+ self.v_bd_matrix = fem.integrate(
+ self._collision_projector_form,
+ fields={
+ "u": self.u_trial,
+ "v": self.u_test,
+ "u_cur": self.u_field,
+ "f": self.force_field,
+ },
+ values=self._collision_projector_args,
+ nodal=True,
+ output_dtype=float,
+ )
+ fem.normalize_dirichlet_projector(self.v_bd_matrix)
+ self.project_constant_forms()
+
+ proj_tape = wp.Tape() if tape is None else tape
+ with proj_tape:
+ diff_bsr_mv(
+ A=self.v_bd_matrix,
+ x=u_rhs,
+ y=u_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ self_adjoint=True,
+ )
+
+ def compute_initial_guess(self):
+ # Self-advect
+ self.du_field.dof_values.zero_()
+ rhs = self.constraint_free_rhs(with_external_forces=False)
+
+ fem.dirichlet.project_system_rhs(self.A, rhs, self.v_bd_matrix, self.v_bd_rhs)
+
+ bsr_cg(self.A_proj, b=rhs, x=self.du_field.dof_values, quiet=True)
+
+ array_axpy(x=self.du_field.dof_values, y=self.u_field.dof_values)
+
+ def scale_lame_field(self, stiffness_scale_array: wp.array):
+ # make a copy of the original field for differentiability
+ wp.launch(
+ scale_lame,
+ dim=self.lame_field.dof_values.shape[0],
+ inputs=[
+ self.lame_field.dof_values,
+ self.lame_ref,
+ stiffness_scale_array,
+ ],
+ )
+
+ def reset_fields(self):
+ self.u_field.dof_values.zero_()
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ parser.add_argument("--degree", type=int, default=1)
+ parser.add_argument(
+ "--serendipity", action=argparse.BooleanOptionalAction, default=False
+ )
+ parser.add_argument("-n", "--n_frames", type=int, default=-1)
+ parser.add_argument("--n_newton", type=int, default=2)
+ parser.add_argument("--newton_tol", type=float, default=1.0e-4)
+ parser.add_argument("--cg_tol", type=float, default=1.0e-8)
+ parser.add_argument("--cg_iters", type=float, default=1000)
+ parser.add_argument("--n_backtrack", type=int, default=4)
+ parser.add_argument("--young_modulus", type=float, default=250.0)
+ parser.add_argument("--poisson_ratio", type=float, default=0.4)
+ parser.add_argument("--gravity", type=float, default=10.0)
+ parser.add_argument("--up_axis", "-up", type=int, default=1)
+ parser.add_argument("--density", type=float, default=1.0)
+ parser.add_argument("--dt", type=float, default=0.1)
+ parser.add_argument(
+ "--quasi_quasistatic", "-qqs", action=argparse.BooleanOptionalAction
+ )
+ parser.add_argument(
+ "-nh", "--neo_hookean", action=argparse.BooleanOptionalAction
+ )
+ parser.add_argument(
+ "-dg", "--discontinuous", action=argparse.BooleanOptionalAction
+ )
+ parser.add_argument("--dg_jump_pen", type=float, default=1.0)
+ parser.add_argument("-ss", "--step_size", type=float, default=0.001)
+ parser.add_argument(
+ "--quiet", action=argparse.BooleanOptionalAction, default=False
+ )
+ parser.add_argument("--lumped_mass", action=argparse.BooleanOptionalAction)
+ parser.add_argument(
+ "--fp64", action=argparse.BooleanOptionalAction, default=False
+ )
+ parser.add_argument(
+ "--matrix_free", action=argparse.BooleanOptionalAction, default=False
+ )
+
+
+class ClassicFEM(SoftbodySim):
+ def __init__(self, geo: fem.Geometry, active_cells: wp.array, args):
+ super().__init__(geo, active_cells, args)
+
+ self._ls = LineSearchUnconstrainedArmijoCriterion(self)
+
+ self._make_elasticity_forms()
+
+ def _make_elasticity_forms(self):
+ if self.args.neo_hookean:
+ self.elastic_energy = ClassicFEM.nh_elastic_energy
+ self.elastic_forces = ClassicFEM.nh_elastic_forces
+ self.elasticity_hessian = ClassicFEM.nh_elasticity_hessian
+ self.stress_field = ClassicFEM.nh_stress_field
+ else:
+ self.elastic_energy = ClassicFEM.cr_elastic_energy
+ self.elastic_forces = ClassicFEM.cr_elastic_forces
+ self.elasticity_hessian = ClassicFEM.cr_elasticity_hessian
+ self.stress_field = ClassicFEM.cr_stress_field
+
+ def supports_matrix_free(self):
+ return True
+
+ def evaluate_energy(self):
+ E_u, c_r = super().evaluate_energy()
+
+ E_e = fem.integrate(
+ self.elastic_energy,
+ quadrature=self.elasticity_quadrature,
+ fields={"u_cur": self.u_field, "lame": self.lame_field},
+ )
+
+ E_tot = E_u + E_e
+
+ return E_tot, c_r
+
+ def init_strain_spaces(self):
+ self.elasticity_quadrature = self.vel_quadrature
+ self.constraint_field = self.interpolated_constraint_field
+ self._constraint_field_restriction = fem.make_restriction(
+ self.constraint_field, space_restriction=self.u_test.space_restriction
+ )
+
+ def set_strain_basis(self, strain_basis: fem.BasisSpace):
+ pass
+
+ def compute_initial_guess(self):
+ # Start from last frame pose, known good state
+ self.du_field.dof_values.zero_()
+
+ def newton_lhs(self):
+ if self.args.matrix_free:
+ return None
+
+ u_matrix = fem.integrate(
+ self.elasticity_hessian,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "u": self.u_trial,
+ "v": self.u_test,
+ "lame": self.lame_field,
+ },
+ output_dtype=float,
+ )
+
+ u_matrix += self.A
+ fem.dirichlet.project_system_matrix(u_matrix, self.v_bd_matrix)
+
+ return u_matrix
+
+ def newton_rhs(self, tape: wp.Tape = None):
+ u_rhs = self.constraint_free_rhs(tape=tape)
+
+ rhs_tape = wp.Tape() if tape is None else tape
+ with rhs_tape:
+ fem.integrate(
+ self.elastic_forces,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "v": self.u_test,
+ "lame": self.lame_field,
+ },
+ output=u_rhs,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ self._minus_dE_du = wp.clone(u_rhs, requires_grad=False)
+
+ self._filter_forces(u_rhs, tape=tape)
+
+ return u_rhs
+
+ @staticmethod
+ def _solve_fp64(lhs, rhs, res, maxiters, tol=None):
+ lhs64 = sp.bsr_copy(lhs, scalar_type=wp.float64)
+ rhs64 = wp.empty(shape=rhs.shape, dtype=wp.vec3d, device=rhs.device)
+ wp.utils.array_cast(in_array=rhs, out_array=rhs64)
+
+ res64 = wp.zeros_like(rhs64)
+ bsr_cg(
+ A=lhs64,
+ b=rhs64,
+ x=res64,
+ quiet=True,
+ tol=tol,
+ max_iters=maxiters,
+ )
+
+ wp.utils.array_cast(in_array=res64, out_array=res)
+
+ return res
+
+ def solve_newton_system(self, lhs, rhs):
+ if self.args.fp64:
+ res = wp.empty_like(rhs)
+ ClassicFEM._solve_fp64(
+ lhs, rhs, res, maxiters=self.args.cg_iters, tol=self.args.cg_tol
+ )
+ return (res,)
+
+ if lhs is None:
+ lhs = self._make_matrix_free_newton_linear_operator()
+ use_diag_precond = False
+ else:
+ use_diag_precond = True
+
+ res = wp.zeros_like(rhs)
+ bsr_cg(
+ A=lhs,
+ b=rhs,
+ x=res,
+ quiet=True,
+ tol=self.args.cg_tol,
+ max_iters=self.args.cg_iters,
+ use_diag_precond=use_diag_precond,
+ )
+ return (res,)
+
+ def record_adjoint(self, tape):
+ # The forward Newton is finding a root of rhs(q, p) = 0 with q = (u, S, R, lambda)
+ # so drhs/dp = drhs/dq dq/dp + drhs/dp = 0
+ # [- drhs/dq] dq/dp = drhs/dp
+ # lhs dq/dp = drhs/dp
+
+ self.prepare_newton_step(tape=tape)
+ rhs = self.newton_rhs(tape=tape)
+ lhs = self.newton_lhs()
+
+ def solve_backward():
+ adj_res = self.u_field.dof_values.grad
+ ClassicFEM._solve_fp64(lhs, adj_res, rhs.grad, maxiters=self.args.cg_iters)
+
+ tape.record_func(
+ solve_backward,
+ arrays=[
+ self.u_field.dof_values,
+ rhs,
+ ],
+ )
+
+ # So we can compute stress-based losses
+ with tape:
+ self.interpolate_constraint_field()
+
+ def interpolate_constraint_field(self, strain=False):
+ field = self.strain_field if strain else self.stress_field
+
+ fem.interpolate(
+ field,
+ fields={
+ "u_cur": self.u_field,
+ "lame": self.lame_field,
+ },
+ kernel_options={"enable_backward": True},
+ dest=self._constraint_field_restriction,
+ )
+
+ @fem.integrand
+ def nh_elastic_energy(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ # return snh_energy(F, lame(s))
+ return nh_energy(F, lame(s))
+
+ @fem.integrand
+ def nh_elastic_forces(s: Sample, u_cur: Field, v: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ tau = fem.grad(v, s)
+
+ # return -wp.ddot(tau, snh_stress(F, lame(s)))
+ return -wp.ddot(tau, nh_stress(F, lame(s)))
+
+ @fem.integrand
+ def nh_stress_field(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ # return snh_stress(F, lame(s))
+ return nh_stress(F, lame(s))
+
+ @fem.integrand
+ def nh_elasticity_hessian(s: Sample, u_cur: Field, u: Field, v: Field, lame: Field):
+ F_s = defgrad(u_cur, s)
+ tau_s = fem.grad(v, s)
+ sig_s = fem.grad(u, s)
+ lame_s = lame(s)
+
+ # return snh_hessian_proj_analytic(F_s, tau_s, sig_s, lame_s)
+ return nh_hessian_proj_analytic(F_s, tau_s, sig_s, lame_s)
+
+ @fem.integrand
+ def cr_elastic_energy(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ S = symmetric_strain(F)
+ return hooke_energy(S, lame(s))
+
+ @fem.integrand
+ def cr_elastic_forces(s: Sample, u_cur: Field, v: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ S = symmetric_strain(F)
+ tau = symmetric_strain_delta(F, fem.grad(v, s))
+ return -wp.ddot(tau, hooke_stress(S, lame(s)))
+
+ @fem.integrand
+ def cr_stress_field(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ S = symmetric_strain(F)
+ return hooke_stress(S, lame(s))
+
+ @fem.integrand
+ def cr_elasticity_hessian(s: Sample, u_cur: Field, u: Field, v: Field, lame: Field):
+ F_s = defgrad(u_cur, s)
+
+ U = wp.mat33()
+ sig = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F_s, U, sig, V)
+
+ S_s = symmetric_strain(sig, V)
+ tau_s = symmetric_strain_delta(U, sig, V, fem.grad(v, s))
+ sig_s = symmetric_strain_delta(U, sig, V, fem.grad(u, s))
+ lame_s = lame(s)
+
+ return hooke_hessian(S_s, tau_s, sig_s, lame_s)
+
+ @fem.integrand
+ def strain_field(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ return symmetric_strain(F)
+
+ @fem.integrand
+ def _polar_decomposition(
+ s: fem.Sample,
+ u: fem.Field,
+ Us: wp.array(dtype=wp.mat33),
+ sigs: wp.array(dtype=wp.vec3),
+ Vs: wp.array(dtype=wp.mat33),
+ ):
+ F = defgrad(u, s)
+
+ U = wp.mat33()
+ D = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, D, V)
+
+ Us[s.qp_index] = U
+ sigs[s.qp_index] = D
+ Vs[s.qp_index] = V
+
+ def _make_matrix_free_newton_linear_operator(self):
+ x_field = self.u_field.space.make_field(
+ space_partition=self.u_field.space_partition
+ )
+
+ if self.args.neo_hookean:
+
+ def matvec(
+ x: wp.array, y: wp.array, z: wp.array, alpha: float, beta: float
+ ):
+ """Compute z = alpha * A @ x + beta * y"""
+ wp.copy(src=x, dest=x_field.dof_values)
+ fem.integrate(
+ self._nh_matrix_free_lhs_form,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "u": x_field,
+ "v": self.u_test,
+ "lame": self.lame_field,
+ },
+ values={"rho": self.args.density, "dt": self.dt},
+ output=z,
+ )
+
+ self._filter_forces(z, tape=None)
+ fem.linalg.array_axpy(x=y, y=z, alpha=beta, beta=alpha)
+
+ else:
+ # precompute polar decomposition, constant over CG iterations
+ U = wp.empty(
+ dtype=wp.mat33, shape=self.elasticity_quadrature.total_point_count()
+ )
+ sig = wp.empty(
+ dtype=wp.vec3, shape=self.elasticity_quadrature.total_point_count()
+ )
+ V = wp.empty(
+ dtype=wp.mat33, shape=self.elasticity_quadrature.total_point_count()
+ )
+
+ fem.interpolate(
+ ClassicFEM._polar_decomposition,
+ quadrature=self.elasticity_quadrature,
+ fields={"u": self.u_field},
+ values={"Us": U, "Vs": V, "sigs": sig},
+ )
+
+ def matvec(
+ x: wp.array, y: wp.array, z: wp.array, alpha: float, beta: float
+ ):
+ """Compute z = alpha * A @ x + beta * y"""
+ wp.copy(src=x, dest=x_field.dof_values)
+ fem.integrate(
+ self._cr_matrix_free_lhs_form,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u": x_field,
+ "v": self.u_test,
+ "lame": self.lame_field,
+ },
+ values={
+ "rho": self.args.density,
+ "dt": self.dt,
+ "Us": U,
+ "Vs": V,
+ "sigs": sig,
+ },
+ output=z,
+ )
+
+ self._filter_forces(z, tape=None)
+ fem.linalg.array_axpy(x=y, y=z, alpha=beta, beta=alpha)
+
+ n = x_field.dof_values.shape[0] * 3
+ linop = LinearOperator(
+ shape=(n, n),
+ dtype=float,
+ device=x_field.dof_values.device,
+ matvec=matvec,
+ )
+ linop._field = x_field # prevent garbage collection
+ return linop
+
+ @fem.integrand
+ def _cr_matrix_free_lhs_form(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+ lame: Field,
+ rho: float,
+ dt: float,
+ Us: wp.array(dtype=wp.mat33),
+ sigs: wp.array(dtype=wp.vec3),
+ Vs: wp.array(dtype=wp.mat33),
+ ):
+ U = Us[s.qp_index]
+ sig = sigs[s.qp_index]
+ V = Vs[s.qp_index]
+
+ S_s = symmetric_strain(sig, V)
+ tau_s = symmetric_strain_delta(U, sig, V, fem.grad(u, s))
+ sig_s = symmetric_strain_delta(U, sig, V, fem.grad(v, s))
+ lame_s = lame(s)
+
+ return hooke_hessian(S_s, tau_s, sig_s, lame_s) + inertia_form(
+ s, domain, u, v, rho, dt
+ )
+
+ @fem.integrand
+ def _nh_matrix_free_lhs_form(
+ s: Sample,
+ domain: Domain,
+ u_cur: Field,
+ u: Field,
+ v: Field,
+ lame: Field,
+ rho: float,
+ dt: float,
+ ):
+ F_s = defgrad(u_cur, s)
+ tau_s = fem.grad(u, s)
+ sig_s = fem.grad(v, s)
+ lame_s = lame(s)
+
+ # return snh_hessian_proj_analytic(F_s, tau_s, sig_s, lame_s) + inertia_form(
+ return nh_hessian_proj_analytic(F_s, tau_s, sig_s, lame_s) + inertia_form(
+ s, domain, u, v, rho, dt
+ )
+
+
+def run_softbody_sim(
+ sim: SoftbodySim,
+ init_callback=None,
+ frame_callback=None,
+ ui=True,
+ log=None,
+ shutdown=False,
+ output_path=None,
+ output_save_volume=True,
+):
+ if log is not None:
+ with open(log, "w") as log_f:
+ sim.log = log_f
+ run_softbody_sim(sim, init_callback, frame_callback, ui=ui, log=None)
+ sim.log = None
+ return
+
+ if not ui:
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ sim.cur_frame = 0
+ if init_callback:
+ init_callback()
+
+ active_indices = sim.u_field.space_partition.space_node_indices().numpy()
+
+ for frame in range(sim.args.n_frames):
+ sim.cur_frame = frame + 1
+ with wp.ScopedTimer(f"--- Frame --- {sim.cur_frame}", synchronize=True):
+ sim.run_frame()
+
+ displaced_pos = sim.u_field.space.node_positions().numpy()
+ displaced_pos[active_indices] += sim.u_field.dof_values.numpy()
+
+ if frame_callback:
+ frame_callback(displaced_pos)
+ return
+
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ active_cells = None if sim.cells is None else sim.cells.array.numpy()
+
+ try:
+ hexes = sim.u_field.space.node_hexes()
+
+ if active_cells is not None:
+ hex_per_cell = len(hexes) // sim.geo.cell_count()
+ selected_hexes = np.broadcast_to(
+ (active_cells * hex_per_cell).reshape(len(active_cells), 1),
+ shape=(len(active_cells), hex_per_cell),
+ )
+ selected_hexes = selected_hexes + np.broadcast_to(
+ np.arange(hex_per_cell).reshape(1, hex_per_cell),
+ shape=(len(active_cells), hex_per_cell),
+ )
+
+ hexes = hexes[selected_hexes.flatten()]
+
+ except AttributeError:
+ hexes = None
+
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+
+ if active_cells is not None:
+ tet_per_cell = len(tets) // sim.geo.cell_count()
+ selected_tets = np.broadcast_to(
+ (active_cells * tet_per_cell).reshape(len(active_cells), 1),
+ shape=(len(active_cells), tet_per_cell),
+ )
+ selected_tets = selected_tets + np.broadcast_to(
+ np.arange(tet_per_cell).reshape(1, tet_per_cell),
+ shape=(len(active_cells), tet_per_cell),
+ )
+
+ tets = tets[selected_tets.flatten()]
+
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ ps.init()
+ if sim.args.ground:
+ ps.set_ground_plane_height(sim.args.ground_height)
+ else:
+ ps.set_ground_plane_mode(mode_str="none")
+
+ node_pos = sim.u_field.space.node_positions().numpy()
+
+ ps_vol = ps.register_volume_mesh(
+ "volume mesh", node_pos, hexes=hexes, tets=tets, edge_width=1.0
+ )
+ ps.register_volume_mesh(
+ "reference mesh",
+ node_pos,
+ hexes=hexes,
+ tets=tets,
+ edge_width=1.0,
+ enabled=False,
+ )
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+ sim.cur_frame = 0
+
+ if init_callback:
+ init_callback()
+
+ active_indices = sim.u_field.space_partition.space_node_indices().numpy()
+
+ faces = sim.geo._face_vertex_indices.numpy()
+ bd_faces = faces[sim.geo._boundary_face_indices.numpy()]
+ surface_pos = sim.geo.positions.numpy()
+
+ if output_path is not None:
+ import os
+
+ os.makedirs(output_path, exist_ok=True)
+ if output_save_volume:
+ pp3d.write_mesh(
+ node_pos, tets, output_path + "/frame_" + str(sim.cur_frame) + ".obj"
+ )
+ else:
+ pp3d.write_mesh(
+ surface_pos,
+ bd_faces,
+ output_path + "/frame_" + str(sim.cur_frame) + ".obj",
+ )
+
+ def callback():
+ global running
+ changed, running = psim.Checkbox("Running", running)
+ if not running:
+ return
+
+ sim.cur_frame = sim.cur_frame + 1
+ if sim.args.n_frames >= 0 and sim.cur_frame > sim.args.n_frames:
+ if shutdown:
+ ps.unshow()
+ return
+
+ with wp.ScopedTimer(f"--- Frame --- {sim.cur_frame}", synchronize=True):
+ sim.run_frame()
+
+ displaced_pos = sim.u_field.space.node_positions().numpy()
+ displaced_pos[active_indices] += sim.u_field.dof_values.numpy()
+ ps_vol.update_vertex_positions(displaced_pos)
+
+ if frame_callback:
+ frame_callback(displaced_pos)
+
+ if output_path is not None:
+ if output_save_volume:
+ # volume mesh
+ pp3d.write_mesh(
+ displaced_pos,
+ tets,
+ output_path + "/frame_" + str(sim.cur_frame) + ".obj",
+ )
+ else:
+ # surface mesh
+ surface_pos = sim.geo.positions.numpy()
+ surface_pos += sim.u_field.dof_values.numpy()[: surface_pos.shape[0]]
+ pp3d.write_mesh(
+ surface_pos,
+ bd_faces,
+ output_path + "/frame_" + str(sim.cur_frame) + ".obj",
+ )
+
+ # ps.screenshot()
+
+ ps.set_user_callback(callback)
+ # ps.look_at(target=(0.5, 0.5, 0.5), camera_location=(0.5, 0.5, 2.5))
+ ps.show()
diff --git a/deps/vomp/simulation/warp.fem/fem_examples/mfem/variable_density.py b/deps/vomp/simulation/warp.fem/fem_examples/mfem/variable_density.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c2c0ad9b6e9b1a6e453061d6c8cc622583979dd
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/fem_examples/mfem/variable_density.py
@@ -0,0 +1,270 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Domain, Sample, Field
+
+from fem_examples.mfem.softbody_sim import (
+ SoftbodySim,
+ ClassicFEM,
+ dg_penalty_form,
+ external_forces_form,
+ VolumetricForces,
+)
+
+
+@fem.integrand
+def inertia_form_density(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+ rho: Field,
+ dt: float,
+):
+ """ with spatially varying density."""
+
+ u_rhs = rho(s) * u(s) / (dt * dt)
+ return wp.dot(u_rhs, v(s))
+
+
+@fem.integrand
+def displacement_rhs_form_density(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ u_prev: Field,
+ v: Field,
+ rho: Field,
+ gravity: wp.vec3,
+ dt: float,
+):
+ """ - + with spatially varying density."""
+
+ f = (
+ inertia_form_density(s, domain, u_prev, v, rho, dt)
+ - inertia_form_density(s, domain, u, v, rho, dt)
+ + rho(s) * wp.dot(gravity, v(s))
+ )
+ return f
+
+
+@fem.integrand
+def kinetic_potential_energy_density(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+ rho: Field,
+ gravity: wp.vec3,
+ dt: float,
+ forces: VolumetricForces,
+):
+ """
+ kinetic+potential energy with spatially varying density.
+
+ 0.5 * -
+ """
+
+ du = u(s) - v(s)
+ kinetic = 0.5 * rho(s) * wp.dot(du, du) / (dt * dt)
+ potential = -rho(s) * wp.dot(gravity, u(s))
+ # Subtract external forces potential energy contribution to match base behavior
+ E = kinetic + potential
+ E -= external_forces_form(s, domain, u, forces)
+ return E
+
+
+class SoftbodySimDensity(SoftbodySim):
+ """Extension of SoftbodySim that supports spatially varying density.
+
+ Adds a scalar density field `rho_field` defined on the same collocated basis as velocity.
+ Overrides mass assembly, RHS, and energy to use the density field.
+ """
+
+ def init_displacement_space(self, side_subdomain: fem.Domain):
+ super().init_displacement_space(side_subdomain)
+
+ # Collocated scalar field for per-vertex density
+ rho_space = fem.make_collocated_function_space(self._vel_basis, dtype=float)
+ self.rho_field = rho_space.make_field(
+ space_partition=self.u_field.space_partition
+ )
+ self.rho_field.dof_values.fill_(self.args.density)
+
+ def set_density_from_array(self, density_per_vertex: np.ndarray):
+ """Assign per-vertex density values (shape must match velocity DOFs)."""
+
+ if density_per_vertex.ndim != 1:
+ raise ValueError(
+ "density_per_vertex must be a 1D array of length = node_count"
+ )
+
+ node_count = self.u_test.space_partition.node_count()
+ if density_per_vertex.shape[0] != node_count:
+ raise ValueError(
+ f"Expected density array of length {node_count}, got {density_per_vertex.shape[0]}"
+ )
+
+ self.rho_field.dof_values = wp.array(density_per_vertex, dtype=float)
+
+ def init_constant_forms(self):
+ args = self.args
+
+ self.update_force_weight()
+
+ if args.matrix_free:
+ self.A = None
+ return
+
+ if self.args.lumped_mass:
+ self.A = fem.integrate(
+ inertia_form_density,
+ fields={"u": self.u_trial, "v": self.u_test, "rho": self.rho_field},
+ values={"dt": self.dt},
+ output_dtype=float,
+ nodal=True,
+ )
+ else:
+ self.A = fem.integrate(
+ inertia_form_density,
+ fields={"u": self.u_trial, "v": self.u_test, "rho": self.rho_field},
+ values={"dt": self.dt},
+ output_dtype=float,
+ quadrature=self.vel_quadrature,
+ )
+
+ if self.side_quadrature is not None:
+ self.A += fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_side_trial, "v": self.u_side_test},
+ values={"k": self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output_dtype=float,
+ )
+
+ if self._penalty_lhs_form:
+ self.A += fem.integrate(
+ self._penalty_lhs_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ output_dtype=float,
+ quadrature=self.vel_quadrature,
+ )
+
+ self.A.nnz_sync()
+
+ def constraint_free_rhs(self, dt=None, with_external_forces=True, tape=None):
+ gravity = self.gravity if with_external_forces else wp.vec3(0.0)
+
+ with_gradient = tape is not None
+ rhs_tape = wp.Tape() if tape is None else tape
+ rhs = wp.zeros(
+ dtype=wp.vec3,
+ requires_grad=with_gradient,
+ shape=self.u_test.space_partition.node_count(),
+ )
+
+ with rhs_tape:
+ fem.integrate(
+ displacement_rhs_form_density,
+ fields={
+ "u": self.du_field,
+ "u_prev": self.du_prev,
+ "v": self.u_test,
+ "rho": self.rho_field,
+ },
+ values={"dt": self._step_dt(), "gravity": gravity},
+ output=rhs,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self.side_quadrature is not None:
+ fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_side_test},
+ values={"k": -self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output=rhs,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self._penalty_rhs_form:
+ fem.integrate(
+ self._penalty_rhs_form,
+ fields={"u": self.u_field, "v": self.u_test},
+ output=rhs,
+ add=True,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if with_external_forces and self.forces.count > 0:
+ fem.integrate(
+ external_forces_form,
+ fields={"v": self.u_test},
+ values={"forces": self.forces},
+ output_dtype=wp.vec3,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": False},
+ output=rhs,
+ add=True,
+ )
+
+ return rhs
+
+ def evaluate_energy(self, E_u=None, cr=None):
+ E_u = fem.integrate(
+ kinetic_potential_energy_density,
+ quadrature=self.vel_quadrature,
+ fields={"u": self.du_field, "v": self.du_prev, "rho": self.rho_field},
+ values={
+ "dt": self._step_dt(),
+ "gravity": self.gravity,
+ "forces": self.forces,
+ },
+ output=E_u,
+ )
+
+ if self.side_quadrature is not None:
+ if wp.types.is_array(E_u):
+ Eu_dg = wp.empty_like(E_u)
+ else:
+ Eu_dg = None
+
+ Eu_dg = fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_field.trace()},
+ values={"k": self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output=Eu_dg,
+ )
+
+ if wp.types.is_array(E_u):
+ fem.utils.array_axpy(y=E_u, x=Eu_dg)
+ else:
+ E_u += Eu_dg
+
+ return E_u, 0.0
+
+
+class ClassicFEMWithDensity(SoftbodySimDensity, ClassicFEM):
+ """ClassicFEM variant that supports spatially varying density via `rho_field`."""
+
+ pass
diff --git a/deps/vomp/simulation/warp.fem/material_loader.py b/deps/vomp/simulation/warp.fem/material_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..d82daa03db74cd7974ffdc386a0a2ba695874944
--- /dev/null
+++ b/deps/vomp/simulation/warp.fem/material_loader.py
@@ -0,0 +1,149 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+import warp as wp
+from vomp.inference.utils import MaterialUpsampler
+
+
+def load_material_data(npz_path: str):
+ """
+ Load material data from npz file.
+
+ Args:
+ npz_path: Path to .npz file containing voxel_data
+
+ Returns:
+ tuple: (voxel_coords, voxel_materials) where:
+ - voxel_coords: (N, 3) array of voxel positions
+ - voxel_materials: (N, 3) array of [E, nu, rho] per voxel
+ """
+ data = np.load(npz_path)
+ voxel_data = data["voxel_data"]
+
+ voxel_coords = np.stack([voxel_data["x"], voxel_data["y"], voxel_data["z"]], axis=1)
+
+ voxel_materials = np.stack(
+ [
+ voxel_data["youngs_modulus"],
+ voxel_data["poissons_ratio"],
+ voxel_data["density"],
+ ],
+ axis=1,
+ )
+
+ print(f"Loaded material data:")
+ print(f" Voxels: {voxel_coords.shape[0]}")
+ print(
+ f" Young's modulus range: [{voxel_materials[:, 0].min():.2e}, {voxel_materials[:, 0].max():.2e}]"
+ )
+ print(
+ f" Poisson's ratio range: [{voxel_materials[:, 1].min():.3f}, {voxel_materials[:, 1].max():.3f}]"
+ )
+ print(
+ f" Density range: [{voxel_materials[:, 2].min():.2f}, {voxel_materials[:, 2].max():.2f}]"
+ )
+
+ return voxel_coords, voxel_materials
+
+
+def apply_spatially_varying_materials(sim, npz_path: str, k_neighbors: int = 1):
+ voxel_coords, voxel_materials = load_material_data(npz_path)
+
+ # Create upsampler
+ upsampler = MaterialUpsampler(voxel_coords, voxel_materials)
+
+ # Get vertex positions from the mesh
+ node_positions = sim.lame_field.space.node_positions()
+ query_points = node_positions.numpy() # Convert warp array to numpy
+
+ print(f"\nInterpolating materials to {query_points.shape[0]} mesh vertices...")
+
+ interpolated_materials, distances = upsampler.interpolate(
+ query_points, k=k_neighbors
+ )
+
+ youngs_modulus_per_vertex = interpolated_materials[:, 0] # (N,) array
+ poisson_ratio_per_vertex = interpolated_materials[:, 1] # (N,) array
+ density_per_vertex = interpolated_materials[:, 2] # (N,) array
+
+ # Convert Young's modulus and Poisson's ratio to Lame parameters
+ # lame[0] = lambda = E * nu / ((1 + nu) * (1 - 2*nu))
+ # lame[1] = mu = E / (2 * (1 + nu))
+ lame_lambda = (youngs_modulus_per_vertex * poisson_ratio_per_vertex) / (
+ (1.0 + poisson_ratio_per_vertex) * (1.0 - 2.0 * poisson_ratio_per_vertex)
+ )
+ lame_mu = youngs_modulus_per_vertex / (2.0 * (1.0 + poisson_ratio_per_vertex))
+
+ # Stack into (N, 2) array for lame parameters [lambda, mu]
+ lame_params = np.stack([lame_lambda, lame_mu], axis=1)
+
+ # Directly set the lame field values (don't use scale_lame_field as it would multiply)
+ # The lame_field.dof_values is a warp array of wp.vec2
+ sim.lame_field.dof_values.assign(wp.array(lame_params, dtype=wp.vec2))
+
+ return {
+ "youngs_modulus": youngs_modulus_per_vertex,
+ "poisson_ratio": poisson_ratio_per_vertex,
+ "density": density_per_vertex,
+ "interpolation_distances": distances,
+ }
+
+
+def visualize_material_distribution(sim, material_stats: dict, output_path: str = None):
+ import matplotlib.pyplot as plt
+
+ fig, axes = plt.subplots(1, 3, figsize=(15, 4))
+
+ # Young's modulus histogram
+ axes[0].hist(
+ material_stats["youngs_modulus"], bins=50, alpha=0.7, edgecolor="black"
+ )
+ axes[0].set_xlabel("Young's Modulus (Pa)")
+ axes[0].set_ylabel("Frequency")
+ axes[0].set_title("Young's Modulus Distribution")
+ axes[0].set_yscale("log")
+ axes[0].grid(True, alpha=0.3)
+
+ # Poisson's ratio histogram
+ axes[1].hist(
+ material_stats["poisson_ratio"],
+ bins=50,
+ alpha=0.7,
+ edgecolor="black",
+ color="orange",
+ )
+ axes[1].set_xlabel("Poisson's Ratio")
+ axes[1].set_ylabel("Frequency")
+ axes[1].set_title("Poisson's Ratio Distribution")
+ axes[1].grid(True, alpha=0.3)
+
+ # Density histogram
+ axes[2].hist(
+ material_stats["density"], bins=50, alpha=0.7, edgecolor="black", color="green"
+ )
+ axes[2].set_xlabel("Density (kg/mยณ)")
+ axes[2].set_ylabel("Frequency")
+ axes[2].set_title("Density Distribution")
+ axes[2].grid(True, alpha=0.3)
+
+ plt.tight_layout()
+
+ if output_path:
+ plt.savefig(output_path, dpi=150, bbox_inches="tight")
+ else:
+ plt.show()
+
+ plt.close()
diff --git a/deps/vomp/train_geometry_encoder.py b/deps/vomp/train_geometry_encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..417ba5b3c83bf22429678ae5241a28fa15be140b
--- /dev/null
+++ b/deps/vomp/train_geometry_encoder.py
@@ -0,0 +1,338 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import json
+import glob
+import argparse
+from easydict import EasyDict as edict
+
+import torch
+import torch.multiprocessing as mp
+import numpy as np
+import random
+from safetensors.torch import load_file
+
+from vomp.datasets.sparse_voxel_materials import SparseVoxelMaterials
+from vomp.models.geometry_encoder import ElasticGeometryEncoder, ElasticSLatVoxelDecoder
+from vomp.models.material_vae.standard_vae import StandardVAE
+from vomp.models.material_vae.beta_tc import TripletVAE
+from vomp.trainers.vae.slat_materials import SLatVaeMaterialsTrainer
+from vomp.utils.dist_utils import setup_dist
+
+
+def find_ckpt(cfg):
+ # Load checkpoint
+ cfg["load_ckpt"] = None
+ if cfg.load_dir != "":
+ if cfg.ckpt == "latest":
+ files = glob.glob(os.path.join(cfg.load_dir, "ckpts", "misc_*.pt"))
+ if len(files) != 0:
+ cfg.load_ckpt = max(
+ [
+ int(os.path.basename(f).split("step")[-1].split(".")[0])
+ for f in files
+ ]
+ )
+ elif cfg.ckpt == "none":
+ cfg.load_ckpt = None
+ else:
+ cfg.load_ckpt = int(cfg.ckpt)
+ return cfg
+
+
+def setup_rng(rank):
+ torch.manual_seed(rank)
+ torch.cuda.manual_seed_all(rank)
+ np.random.seed(rank)
+ random.seed(rank)
+
+
+def get_model_summary(model):
+ model_summary = "Parameters:\n"
+ model_summary += "=" * 128 + "\n"
+ model_summary += f'{"Name":<{72}}{"Shape":<{32}}{"Type":<{16}}{"Grad"}\n'
+ num_params = 0
+ num_trainable_params = 0
+ for name, param in model.named_parameters():
+ model_summary += f"{name:<{72}}{str(param.shape):<{32}}{str(param.dtype):<{16}}{param.requires_grad}\n"
+ num_params += param.numel()
+ if param.requires_grad:
+ num_trainable_params += param.numel()
+ model_summary += "\n"
+ model_summary += f"Number of parameters: {num_params}\n"
+ model_summary += f"Number of trainable parameters: {num_trainable_params}\n"
+ return model_summary
+
+
+def main(local_rank, cfg):
+ # Set up distributed training
+ rank = cfg.node_rank * cfg.num_gpus + local_rank
+ world_size = cfg.num_nodes * cfg.num_gpus
+ if world_size > 1:
+ setup_dist(rank, local_rank, world_size, cfg.master_addr, cfg.master_port)
+
+ # Seed rngs
+ setup_rng(rank)
+
+ # Determine normalization parameters file path
+ normalization_params_file = None
+ if cfg.get("matvae_checkpoint") is not None:
+ # Extract the directory containing the matvae checkpoint
+ import os
+
+ matvae_checkpoint_path = cfg.matvae_checkpoint
+
+ # Navigate to the project directory (up from checkpoints/checkpoint_X/)
+ if "checkpoints" in matvae_checkpoint_path:
+ # For paths like: outputs/matvae/checkpoints/checkpoint_821/model.safetensors
+ # Need to go up 3 levels: model.safetensors -> checkpoint_X -> checkpoints -> project_dir
+ matvae_project_dir = os.path.dirname(
+ os.path.dirname(os.path.dirname(matvae_checkpoint_path))
+ )
+ normalization_params_file = os.path.join(
+ matvae_project_dir, "normalization_params.json"
+ )
+ else:
+ # For other checkpoint path formats, try to find the normalization params
+ checkpoint_dir = os.path.dirname(matvae_checkpoint_path)
+ normalization_params_file = os.path.join(
+ checkpoint_dir, "normalization_params.json"
+ )
+
+ if os.path.exists(normalization_params_file):
+ if rank == 0:
+ print(
+ f"Found normalization parameters file: {normalization_params_file}"
+ )
+ else:
+ if rank == 0:
+ print(
+ f"ERROR: Could not find normalization parameters file at {normalization_params_file}"
+ )
+ print(
+ "This file is required for consistent normalization between matvae and geometry encoder training!"
+ )
+ print(
+ "Make sure the matvae was trained with the updated train_material_vae.py that saves normalization_params.json"
+ )
+ raise FileNotFoundError(
+ f"Required normalization parameters file not found: {normalization_params_file}"
+ )
+ else:
+ if rank == 0:
+ print("ERROR: No matvae_checkpoint specified in config!")
+ print(
+ "The matvae_checkpoint is required to locate the normalization parameters file."
+ )
+ raise ValueError(
+ "matvae_checkpoint must be specified in config to load normalization parameters"
+ )
+
+ # Load data
+ train_dataset_args = cfg.dataset.args.copy()
+ train_dataset_args["split"] = "train"
+
+ # Add normalization type if specified in config
+ if "normalization_type" in cfg.dataset:
+ train_dataset_args["normalization_type"] = cfg.dataset.normalization_type
+ if rank == 0:
+ print(
+ f"Using material normalization type: {cfg.dataset.normalization_type}"
+ )
+
+ # Add normalization parameters file (now mandatory)
+ train_dataset_args["normalization_params_file"] = normalization_params_file
+
+ dataset = SparseVoxelMaterials(**train_dataset_args)
+
+ # Create validation dataset
+ val_dataset_args = cfg.dataset.args.copy()
+ val_dataset_args["split"] = "val"
+
+ # Add normalization type if specified in config
+ if "normalization_type" in cfg.dataset:
+ val_dataset_args["normalization_type"] = cfg.dataset.normalization_type
+
+ # Add normalization parameters file (now mandatory)
+ val_dataset_args["normalization_params_file"] = normalization_params_file
+
+ val_dataset = SparseVoxelMaterials(**val_dataset_args)
+
+ # Build trainable models (only geometry encoder)
+ model_dict = {
+ "geometry_encoder": (
+ ElasticGeometryEncoder(**cfg.models.geometry_encoder.args).cuda()
+ ),
+ }
+
+ # Add decoder if specified in config
+ if "decoder" in cfg.models:
+ model_dict["decoder"] = ElasticSLatVoxelDecoder(
+ **cfg.models.decoder.args
+ ).cuda()
+
+ # Load frozen matvae model separately
+ from vomp.models.material_vae.beta_tc import TripletVAE
+
+ matvae = TripletVAE(**cfg.models.matvae.args).cuda()
+
+ # Load matvae checkpoint if provided
+ if cfg.get("matvae_checkpoint") is not None:
+ if rank == 0:
+ print(f"Loading matvae checkpoint from: {cfg.matvae_checkpoint}")
+
+ # Load safetensors checkpoint
+ checkpoint = load_file(cfg.matvae_checkpoint)
+ matvae.load_state_dict(checkpoint, strict=True)
+
+ if rank == 0:
+ print("Successfully loaded matvae checkpoint")
+
+ matvae.eval()
+ for param in matvae.parameters():
+ param.requires_grad = False
+
+ # Model summary
+ if rank == 0:
+ for name, backbone in model_dict.items():
+ model_summary = get_model_summary(backbone)
+ print(f"\n\nBackbone: {name}\n" + model_summary)
+ with open(
+ os.path.join(cfg.output_dir, f"{name}_model_summary.txt"), "w"
+ ) as fp:
+ print(model_summary, file=fp)
+
+ # Also print matvae summary
+ matvae_summary = get_model_summary(matvae)
+ print(f"\n\nFrozen Model: matvae\n" + matvae_summary)
+ with open(os.path.join(cfg.output_dir, "matvae_model_summary.txt"), "w") as fp:
+ print(matvae_summary, file=fp)
+
+ # Build trainer
+ trainer = SLatVaeMaterialsTrainer(
+ model_dict,
+ dataset,
+ matvae=matvae, # Pass matvae as a separate argument
+ val_dataset=val_dataset, # Pass validation dataset
+ trellis_weights_path=cfg.get(
+ "trellis_weights_path"
+ ), # Pass TRELLIS weights path from config
+ training_mode=cfg.get("training_mode", "encoder_only"), # Add training mode
+ **cfg.trainer.args,
+ output_dir=cfg.output_dir,
+ load_dir=cfg.load_dir,
+ step=cfg.load_ckpt,
+ )
+
+ # Train
+ if not cfg.tryrun:
+ if cfg.profile:
+ trainer.profile()
+ else:
+ trainer.run()
+
+
+if __name__ == "__main__":
+ # Arguments and config
+ parser = argparse.ArgumentParser()
+ ## config
+ parser.add_argument(
+ "--config", type=str, required=True, help="Experiment config file"
+ )
+ ## io and resume
+ parser.add_argument(
+ "--output_dir", type=str, required=True, help="Output directory"
+ )
+ parser.add_argument(
+ "--load_dir", type=str, default="", help="Load directory, default to output_dir"
+ )
+ parser.add_argument(
+ "--ckpt",
+ type=str,
+ default="latest",
+ help="Checkpoint step to resume training, default to latest",
+ )
+ parser.add_argument(
+ "--data_dir", type=str, default="./data/", help="Data directory"
+ )
+ parser.add_argument(
+ "--auto_retry", type=int, default=3, help="Number of retries on error"
+ )
+ ## dubug
+ parser.add_argument(
+ "--tryrun", action="store_true", help="Try run without training"
+ )
+ parser.add_argument("--profile", action="store_true", help="Profile training")
+ ## multi-node and multi-gpu
+ parser.add_argument("--num_nodes", type=int, default=1, help="Number of nodes")
+ parser.add_argument("--node_rank", type=int, default=0, help="Node rank")
+ parser.add_argument(
+ "--num_gpus",
+ type=int,
+ default=-1,
+ help="Number of GPUs per node, default to all",
+ )
+ parser.add_argument(
+ "--master_addr",
+ type=str,
+ default="localhost",
+ help="Master address for distributed training",
+ )
+ parser.add_argument(
+ "--master_port", type=str, default="12345", help="Port for distributed training"
+ )
+ opt = parser.parse_args()
+ opt.load_dir = opt.load_dir if opt.load_dir != "" else opt.output_dir
+ opt.num_gpus = torch.cuda.device_count() if opt.num_gpus == -1 else opt.num_gpus
+ ## Load config
+ config = json.load(open(opt.config, "r"))
+ ## Combine arguments and config
+ cfg = edict()
+ cfg.update(opt.__dict__)
+ cfg.update(config)
+ print("\n\nConfig:")
+ print("=" * 80)
+ print(json.dumps(cfg.__dict__, indent=4))
+
+ # Prepare output directory
+ if cfg.node_rank == 0:
+ os.makedirs(cfg.output_dir, exist_ok=True)
+ ## Save command and config
+ with open(os.path.join(cfg.output_dir, "command.txt"), "w") as fp:
+ print(" ".join(["python"] + sys.argv), file=fp)
+ with open(os.path.join(cfg.output_dir, "config.json"), "w") as fp:
+ json.dump(config, fp, indent=4)
+
+ # Run
+ if cfg.auto_retry == 0:
+ cfg = find_ckpt(cfg)
+ if cfg.num_gpus > 1:
+ mp.spawn(main, args=(cfg,), nprocs=cfg.num_gpus, join=True)
+ else:
+ main(0, cfg)
+ else:
+ for rty in range(cfg.auto_retry):
+ try:
+ cfg = find_ckpt(cfg)
+ if cfg.num_gpus > 1:
+ mp.spawn(main, args=(cfg,), nprocs=cfg.num_gpus, join=True)
+ else:
+ main(0, cfg)
+ break
+ except Exception as e:
+ print(f"Error: {e}")
+ print(f"Retrying ({rty + 1}/{cfg.auto_retry})...")
diff --git a/deps/vomp/train_material_vae.py b/deps/vomp/train_material_vae.py
new file mode 100755
index 0000000000000000000000000000000000000000..d37448bba89e3cb89f691b2d9c57bb2466fcbf5b
--- /dev/null
+++ b/deps/vomp/train_material_vae.py
@@ -0,0 +1,1219 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+import json, math, argparse, shutil, itertools, os
+import numpy as np
+from pathlib import Path
+from typing import Tuple, Dict, Any, List, Optional
+
+import pandas as pd
+import torch
+import torch.nn as nn
+from torch.utils.data import DataLoader, TensorDataset, random_split
+from torch.distributions import Normal
+
+from accelerate import Accelerator
+from accelerate.utils import (
+ DataLoaderConfiguration,
+ ProjectConfiguration,
+ set_seed,
+ DistributedDataParallelKwargs,
+ TorchDynamoPlugin,
+)
+from accelerate.logging import get_logger
+from tqdm.auto import tqdm
+
+from vomp.models.material_vae.beta_tc import TripletVAE
+from vomp.models.material_vae.standard_vae import StandardVAE
+
+# Set up logger
+logger = get_logger(__name__)
+
+
+class TensorState:
+ """Simple wrapper to make tensors checkpointable with state_dict/load_state_dict."""
+
+ def __init__(self, tensor: torch.Tensor, name: str = "tensor"):
+ self.name = name
+ self.tensor = tensor
+ self.device = tensor.device
+
+ def state_dict(self):
+ return {self.name: self.tensor.cpu()} # Save on CPU to avoid device issues
+
+ def load_state_dict(self, state_dict):
+ self.tensor = state_dict[self.name].to(self.device) # Move to correct device
+
+ def to(self, device):
+ self.tensor = self.tensor.to(device)
+ self.device = device
+ return self
+
+ def __getattr__(self, name):
+ # Delegate attribute access to the underlying tensor
+ return getattr(self.tensor, name)
+
+
+class EpochTracker:
+ """Tracks the current epoch for checkpointing."""
+
+ def __init__(self, epoch: int = 0):
+ self.epoch = epoch
+
+ def state_dict(self):
+ return {"epoch": self.epoch}
+
+ def load_state_dict(self, state_dict):
+ self.epoch = state_dict["epoch"]
+
+ def increment(self):
+ self.epoch += 1
+
+ def __int__(self):
+ return self.epoch
+
+ def __str__(self):
+ return str(self.epoch)
+
+
+def physics_aware_transform(
+ E: torch.Tensor,
+ nu: torch.Tensor,
+ rho: torch.Tensor,
+ normalization_type: str = "standard",
+ nu_min: float = None,
+ nu_max: float = None,
+) -> torch.Tensor:
+ """
+ Transform material properties with physics-aware normalization.
+
+ Args:
+ E: Young's modulus (Pa)
+ nu: Poisson's ratio (dimensionless)
+ rho: Density (kg/mยณ)
+ normalization_type: "standard", "minmax", "physics_bounds", or "log_minmax"
+ nu_min, nu_max: Required for "standard" normalization
+ """
+ # Add safety check for non-positive values
+ E = torch.clamp_min(E, 1e-8)
+ rho = torch.clamp_min(rho, 1e-8)
+
+ if normalization_type == "standard":
+ # Original approach - may break material relationships
+ if nu_min is None or nu_max is None:
+ nu_min, nu_max = nu.min().item(), nu.max().item()
+ yE = torch.log10(E)
+ p = (nu - nu_min) / (nu_max - nu_min)
+ p = p.clamp(1e-4, 1.0 - 1e-4)
+ y_pr = torch.logit(p)
+ y_rho = torch.log10(rho)
+ return torch.stack([yE, y_pr, y_rho], -1)
+
+ elif normalization_type == "physics_bounds":
+ # Use known physical bounds for materials
+ # E: 1 MPa (soft polymers) to 1000 GPa (diamond)
+ E_norm = (torch.log10(E) - 6.0) / (12.0 - 6.0) # log10(1e6) to log10(1e12)
+
+ # ฮฝ: -1 (auxetic) to 0.5 (incompressible)
+ nu_norm = (nu + 1.0) / 1.5 # [-1, 0.5] โ [0, 1]
+
+ # ฯ: 0.1 kg/mยณ (aerogels) to 20000 kg/mยณ (tungsten)
+ rho_norm = (torch.log10(rho) - (-1.0)) / (
+ 4.3 - (-1.0)
+ ) # log10(0.1) to log10(20000)
+
+ return torch.stack([E_norm, nu_norm, rho_norm], -1)
+
+ elif normalization_type == "log_minmax":
+ # Log transform then min-max normalize - preserves relative relationships
+ log_E = torch.log10(E)
+ log_rho = torch.log10(rho)
+
+ # Min-max normalize to [0, 1] - preserves material clustering
+ E_norm = (log_E - log_E.min()) / (log_E.max() - log_E.min())
+ nu_norm = (nu - nu.min()) / (nu.max() - nu.min())
+ rho_norm = (log_rho - log_rho.min()) / (log_rho.max() - log_rho.min())
+
+ return torch.stack([E_norm, nu_norm, rho_norm], -1)
+
+ elif normalization_type == "log_minmax_no_density":
+ # Log transform E but not rho, then min-max normalize - hybrid approach
+ log_E = torch.log10(E)
+
+ # Min-max normalize to [0, 1] - preserves material clustering
+ E_norm = (log_E - log_E.min()) / (log_E.max() - log_E.min())
+ nu_norm = (nu - nu.min()) / (nu.max() - nu.min())
+ rho_norm = (rho - rho.min()) / (rho.max() - rho.min()) # No log for density
+
+ return torch.stack([E_norm, nu_norm, rho_norm], -1)
+
+ elif normalization_type == "minmax":
+ # Simple min-max normalization - preserves ALL relationships
+ E_norm = (E - E.min()) / (E.max() - E.min())
+ nu_norm = (nu - nu.min()) / (nu.max() - nu.min())
+ rho_norm = (rho - rho.min()) / (rho.max() - rho.min())
+
+ return torch.stack([E_norm, nu_norm, rho_norm], -1)
+
+ else:
+ raise ValueError(f"Unknown normalization_type: {normalization_type}")
+
+
+def physics_aware_inverse_transform(
+ y_norm: torch.Tensor,
+ E_min: float,
+ E_max: float,
+ nu_min: float,
+ nu_max: float,
+ rho_min: float,
+ rho_max: float,
+ normalization_type: str = "standard",
+ mu: torch.Tensor = None,
+ std: torch.Tensor = None,
+) -> torch.Tensor:
+ """Inverse transform back to physical units."""
+
+ if normalization_type == "physics_bounds":
+ # Inverse of physics_bounds normalization
+ log_E = y_norm[..., 0] * (12.0 - 6.0) + 6.0
+ E = torch.pow(10, log_E)
+
+ nu = y_norm[..., 1] * 1.5 - 1.0
+
+ log_rho = y_norm[..., 2] * (4.3 - (-1.0)) + (-1.0)
+ rho = torch.pow(10, log_rho)
+
+ elif normalization_type == "log_minmax":
+ # Inverse of log_minmax normalization
+ log_E_min, log_E_max = torch.log10(torch.tensor(E_min)), torch.log10(
+ torch.tensor(E_max)
+ )
+ log_rho_min, log_rho_max = torch.log10(torch.tensor(rho_min)), torch.log10(
+ torch.tensor(rho_max)
+ )
+
+ log_E = y_norm[..., 0] * (log_E_max - log_E_min) + log_E_min
+ E = torch.pow(10, log_E)
+
+ nu = y_norm[..., 1] * (nu_max - nu_min) + nu_min
+
+ log_rho = y_norm[..., 2] * (log_rho_max - log_rho_min) + log_rho_min
+ rho = torch.pow(10, log_rho)
+
+ elif normalization_type == "log_minmax_no_density":
+ # Inverse of log_minmax_no_density normalization
+ log_E_min, log_E_max = torch.log10(torch.tensor(E_min)), torch.log10(
+ torch.tensor(E_max)
+ )
+
+ log_E = y_norm[..., 0] * (log_E_max - log_E_min) + log_E_min
+ E = torch.pow(10, log_E)
+
+ nu = y_norm[..., 1] * (nu_max - nu_min) + nu_min
+
+ # No log for density - direct min-max inverse
+ rho = y_norm[..., 2] * (rho_max - rho_min) + rho_min
+
+ elif normalization_type == "minmax":
+ # Inverse of minmax normalization
+ E = y_norm[..., 0] * (E_max - E_min) + E_min
+ nu = y_norm[..., 1] * (nu_max - nu_min) + nu_min
+ rho = y_norm[..., 2] * (rho_max - rho_min) + rho_min
+
+ else:
+ # Standard approach (original) - requires mu and std
+ if mu is None or std is None:
+ raise ValueError("mu and std are required for standard normalization")
+ return inverse_transform(y_norm, mu, std, nu_min, nu_max)
+
+ return torch.stack([E, nu, rho], -1)
+
+
+def forward_transform(
+ E: torch.Tensor, nu: torch.Tensor, rho: torch.Tensor, nu_min: float, nu_max: float
+) -> torch.Tensor:
+ # Add safety check for non-positive values
+ E = torch.clamp_min(E, 1e-8)
+ rho = torch.clamp_min(rho, 1e-8)
+
+ yE = torch.log10(E)
+
+ p = (nu - nu_min) / (nu_max - nu_min)
+
+ p = p.clamp(1e-4, 1.0 - 1e-4)
+ y_pr = torch.logit(p)
+ y_rho = torch.log10(rho)
+ return torch.stack([yE, y_pr, y_rho], -1)
+
+
+def compute_stats(
+ dataset: TensorDataset, nu_min: float, nu_max: float, batch_size: int = 1024
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ loader = DataLoader(dataset, batch_size=batch_size)
+ sum_, sum2, n = torch.zeros(3), torch.zeros(3), 0
+ for batch in loader:
+ y = batch[0] # Since TensorDataset returns tuples, get the first element
+ sum_ += y.sum(0)
+ sum2 += (y**2).sum(0)
+ n += y.size(0)
+ mu = sum_ / n
+ std = (sum2 / n - mu**2).sqrt()
+ return mu, std
+
+
+def inverse_transform(
+ y_norm: torch.Tensor,
+ mu: torch.Tensor,
+ std: torch.Tensor,
+ nu_min: float,
+ nu_max: float,
+) -> torch.Tensor:
+ y = y_norm * std + mu
+ E = torch.pow(10, y[..., 0])
+ pr = torch.sigmoid(y[..., 1]) * (nu_max - nu_min) + nu_min
+ rho = torch.pow(10, y[..., 2])
+ return torch.stack([E, pr, rho], -1)
+
+
+@torch.no_grad()
+def iwae_nll(model: nn.Module, loader: DataLoader, K: int = 50) -> float:
+ """Compute IWAE negative log-likelihood estimate.
+
+ Args:
+ model: VAE model (can be TripletVAE or StandardVAE)
+ loader: DataLoader for evaluation
+ K: Number of importance samples
+
+ Returns:
+ Negative log-likelihood estimate (lower is better)
+ """
+ total_log_w, total = 0.0, 0
+ device = next(model.parameters()).device
+
+ for batch in loader:
+ x_norm = batch[0] if isinstance(batch, (tuple, list)) else batch
+ B = x_norm.size(0)
+
+ # Sample from prior
+ z = model.sample_prior(K * B).to(device) # (KยทB, z_dim)
+
+ # Decode
+ (E_mu, Elog_sigma_2), (pr_mu, prlog_sigma_2), (rho_mu, rholog_sigma_2) = (
+ model.decode(z)
+ )
+
+ # Expand input for K samples
+ x_exp = x_norm.repeat(K, 1) # (KยทB, 3)
+
+ # Compute log p(x|z)
+ log_px_z = Normal(E_mu, torch.exp(0.5 * Elog_sigma_2)).log_prob(x_exp[:, 0])
+ log_px_z += Normal(pr_mu, torch.exp(0.5 * prlog_sigma_2)).log_prob(x_exp[:, 1])
+ log_px_z += Normal(rho_mu, torch.exp(0.5 * rholog_sigma_2)).log_prob(
+ x_exp[:, 2]
+ )
+ log_px_z = log_px_z.sum(-1)
+
+ # Compute log p(z)
+ log_pz = Normal(0, 1).log_prob(z).sum(-1)
+
+ # Importance weights
+ log_w = (log_px_z + log_pz).view(K, B) - math.log(K)
+
+ total_log_w += torch.logsumexp(log_w, 0).sum()
+ total += B
+
+ return (-total_log_w / total).item() # lower is better
+
+
+def reconstruction_metrics(
+ x_phys: torch.Tensor, x_recon_phys: torch.Tensor # (B,3)
+) -> Dict[str, float]:
+ diff = x_recon_phys - x_phys
+ rmse = diff.pow(2).mean(0).sqrt() # (3,)
+ rel = (diff.abs() / x_phys.clamp_min(1e-8)).mean(0) # (3,)
+ return {
+ "rmse_E": rmse[0].item(),
+ "rmse_nu": rmse[1].item(),
+ "rmse_rho": rmse[2].item(),
+ "rel_E": rel[0].item(),
+ "rel_nu": rel[1].item(),
+ "rel_rho": rel[2].item(),
+ }
+
+
+def compute_tc_terms(
+ z: torch.Tensor,
+ mu: torch.Tensor,
+ logvar: torch.Tensor,
+ log_q_zx: torch.Tensor,
+) -> Tuple[torch.Tensor, ...]:
+ """Return MI, TC and dimension-wise KL.
+
+ Args:
+ z: (B, D) sampled latent codes.
+ mu: (B, D) posterior means.
+ logvar: (B, D) posterior log-variances.
+ log_q_zx: (B,) log q(z|x) for each sample (already summed over D).
+ """
+ B, D = z.size()
+
+ # pairwise log prob under diagonal Gaussians
+ z_exp = z.unsqueeze(1) # (B,1,D)
+ mu_exp = mu.unsqueeze(0) # (1,B,D)
+ logvar_exp = logvar.unsqueeze(0) # (1,B,D)
+ var_exp = torch.exp(logvar_exp) # (1,B,D)
+
+ # log q(z_i | x_j) for every pair (i,j) and every dimension d
+ const = math.log(2.0 * math.pi)
+ log_q_zi_xj_d = -0.5 * (
+ (z_exp - mu_exp) ** 2 / var_exp + logvar_exp + const
+ ) # (B,B,D)
+
+ # Aggregated posterior log-prob log q(z_i)
+ log_q_zi_xj = log_q_zi_xj_d.sum(-1) # (B,B)
+ log_qz = torch.logsumexp(log_q_zi_xj, dim=1) - math.log(B) # (B,)
+
+ # Marginal log-prob product \sum_d log q(z_i_d)
+ log_qz_prod = torch.zeros_like(log_qz)
+ log_pz_prod = torch.zeros_like(log_qz)
+ for d in range(D):
+ log_q_zi_xj_dim = log_q_zi_xj_d[:, :, d] # (B,B)
+ log_q_zd = torch.logsumexp(log_q_zi_xj_dim, dim=1) - math.log(B) # (B,)
+ log_qz_prod += log_q_zd
+ log_pz_prod += Normal(0, 1).log_prob(z[:, d])
+
+ # Standard normal log-prob over full latent vector
+ log_pz = Normal(0, 1).log_prob(z).sum(dim=1)
+
+ mi = torch.clamp((log_q_zx - log_qz).mean(), min=0.0) # Mutual information
+ tc = (log_qz - log_qz_prod).mean() # Total correlation
+ kl_dim = (log_qz_prod - log_pz_prod).mean() # Dimension-wise KL
+ return mi, tc, kl_dim
+
+
+def rotate_checkpoints(save_dir: Path, keep_last_n: int):
+ ckpts = sorted(save_dir.glob("checkpoint-*"), key=lambda p: p.stat().st_mtime)
+ for p in ckpts[:-keep_last_n]:
+ shutil.rmtree(p, ignore_errors=True)
+
+
+def make_std_dataset(
+ subset: torch.utils.data.Subset, mu: torch.Tensor, std: torch.Tensor
+) -> TensorDataset:
+ idx = torch.tensor(subset.indices, dtype=torch.long)
+ y = subset.dataset.tensors[0][idx] # (N, 3)
+ y_std = (y - mu) / std
+ return TensorDataset(y_std)
+
+
+def compute_perplexity(z: torch.Tensor) -> float:
+ N = z.size(0)
+ # pairwise distances
+ diff = z.unsqueeze(1) - z.unsqueeze(0) # (N, N, z_dim)
+ dist_sq = diff.pow(2).sum(dim=-1) # (N, N)
+
+ # gaussian kernel (sigma=1.0)
+ kernel = torch.exp(-0.5 * dist_sq) # (N, N)
+
+ kernel_sum = kernel.sum(dim=1, keepdim=True) # (N, 1)
+ kernel_norm = kernel / kernel_sum # (N, N)
+
+ entropy = -torch.sum(kernel_norm * torch.log2(kernel_norm + 1e-8)) / N
+ return 2.0 ** entropy.item()
+
+
+def compute_latent_statistics(model, loader: DataLoader) -> Dict[str, float]:
+ """Compute statistics about the learned latent space.
+
+ Args:
+ model: VAE model
+ loader: DataLoader for evaluation
+
+ Returns:
+ Dictionary of latent space statistics
+ """
+ all_z = []
+ all_mu = []
+ all_logvar = []
+
+ with torch.no_grad():
+ for batch in loader:
+ x_norm = batch[0] if isinstance(batch, (tuple, list)) else batch
+ z, mu, logvar = model.encode(x_norm)
+ all_z.append(z)
+ all_mu.append(mu)
+ all_logvar.append(logvar)
+
+ all_z = torch.cat(all_z, dim=0)
+ all_mu = torch.cat(all_mu, dim=0)
+ all_logvar = torch.cat(all_logvar, dim=0)
+
+ mu_var = all_mu.var(dim=0)
+ active_units = (mu_var > 0.01).sum().item()
+
+ perplexity = compute_perplexity(all_z)
+
+ avg_var = torch.exp(all_logvar).mean().item()
+
+ kl_per_dim = 0.5 * (all_mu.pow(2) + torch.exp(all_logvar) - all_logvar - 1.0).mean(
+ dim=0
+ )
+
+ return {
+ "active_units": active_units,
+ "perplexity": perplexity,
+ "avg_posterior_variance": avg_var,
+ "total_kl": kl_per_dim.sum().item(),
+ "max_dim_kl": kl_per_dim.max().item(),
+ "min_dim_kl": kl_per_dim.min().item(),
+ }
+
+
+def train(config: Dict[str, Any]):
+ # Set seed for reproducibility
+ if "seed" in config:
+ set_seed(config["seed"])
+
+ # Configure DataLoader with stateful support
+ dataloader_config = DataLoaderConfiguration(
+ non_blocking=True,
+ use_stateful_dataloader=config.get("use_stateful_dataloader", True),
+ )
+
+ # Configure DDP if needed
+ ddp_kwargs = DistributedDataParallelKwargs(
+ find_unused_parameters=config.get("find_unused_parameters", False)
+ )
+
+ # Configure torch.compile if enabled
+ dynamo_plugin = None
+ if config.get("compile", {}).get("enabled", False):
+ compile_config = config.get("compile", {})
+ dynamo_plugin = TorchDynamoPlugin(
+ backend=compile_config.get("backend", "inductor"),
+ mode=compile_config.get("mode", "default"),
+ fullgraph=compile_config.get("fullgraph", True),
+ dynamic=compile_config.get("dynamic", False),
+ )
+
+ project_dir = Path(config["project_dir"]).resolve()
+ is_dry = config.get("dry_run", False)
+
+ # Initialize accelerator with all features
+ accelerator = Accelerator(
+ mixed_precision=config.get("mixed_precision", "no"), # "no", "fp16", "bf16"
+ log_with=None if is_dry else config.get("log_with", "tensorboard"),
+ gradient_accumulation_steps=config.get("gradient_accumulation_steps", 1),
+ project_config=ProjectConfiguration(
+ project_dir=str(project_dir),
+ automatic_checkpoint_naming=True,
+ total_limit=config.get("keep_last_checkpoints", 2),
+ ),
+ dataloader_config=dataloader_config,
+ kwargs_handlers=[ddp_kwargs] if torch.cuda.is_available() else None,
+ dynamo_plugin=dynamo_plugin,
+ )
+
+ # Initialize trackers
+ if accelerator.is_main_process and not is_dry:
+ accelerator.init_trackers(
+ project_name=config["tracker_name"],
+ config={
+ "batch_size": config["dataloader"]["batch_size"],
+ "learning_rate": config["optimizer"]["lr"],
+ "weight_decay": config["optimizer"]["weight_decay"],
+ "epochs": config["epochs"],
+ "grad_clip_norm": config["optimizer"]["grad_clip_norm"],
+ "free_nats": config["free_nats"],
+ "alpha": config.get("alpha", 1.0),
+ "beta": config.get("beta", 1.0),
+ "gamma": config.get("gamma", 1.0),
+ "standard_vae": config.get("standard_vae", False),
+ "iwae_K": config.get("iwae_K", 50),
+ "mixed_precision": config.get("mixed_precision", "no"),
+ "seed": config.get("seed", None),
+ **config.get("model", {}),
+ },
+ )
+
+ # Load and prepare data
+ data_csv = Path(config["data_csv"])
+ csv_path = data_csv if data_csv.is_absolute() else Path.cwd() / data_csv
+ df = pd.read_csv(csv_path)
+ E_t = torch.tensor(df["youngs_modulus"].values, dtype=torch.float32)
+ nu_t = torch.tensor(df["poisson_ratio"].values, dtype=torch.float32)
+ rho_t = torch.tensor(df["density"].values, dtype=torch.float32)
+
+ nu_min, nu_max = nu_t.min().item(), nu_t.max().item()
+ E_min, E_max = E_t.min().item(), E_t.max().item()
+ rho_min, rho_max = rho_t.min().item(), rho_t.max().item()
+
+ # Choose normalization scheme
+ normalization_type = config.get("normalization_type", "standard")
+
+ if normalization_type == "standard":
+ # Original approach with standardization
+ y_all = forward_transform(E_t, nu_t, rho_t, nu_min, nu_max)
+ dataset_raw = TensorDataset(y_all)
+
+ n_total = len(dataset_raw)
+ n_train = int(0.9 * n_total)
+ n_val = n_total - n_train
+ train_raw, val_raw = random_split(dataset_raw, [n_train, n_val])
+
+ mu, std = compute_stats(train_raw, nu_min, nu_max)
+ train_ds = make_std_dataset(train_raw, mu, std)
+ val_ds = make_std_dataset(val_raw, mu, std)
+
+ # Store normalization parameters for inverse transform
+ norm_params = {
+ "mu": mu,
+ "std": std,
+ "nu_min": nu_min,
+ "nu_max": nu_max,
+ "normalization_type": normalization_type,
+ }
+
+ # Save normalization parameters to JSON file
+ if accelerator.is_main_process:
+ norm_params_path = project_dir / "normalization_params.json"
+
+ # Convert tensors to Python floats for JSON serialization
+ norm_params_json = {
+ "E_min": float(E_min),
+ "E_max": float(E_max),
+ "nu_min": float(nu_min),
+ "nu_max": float(nu_max),
+ "rho_min": float(rho_min),
+ "rho_max": float(rho_max),
+ "normalization_type": normalization_type,
+ "dataset_size": n_total,
+ "train_size": n_train,
+ "val_size": n_val,
+ "mu": [float(x) for x in mu.tolist()],
+ "std": [float(x) for x in std.tolist()],
+ }
+
+ with open(norm_params_path, "w") as f:
+ json.dump(norm_params_json, f, indent=2)
+
+ logger.info(f"Saved normalization parameters to {norm_params_path}")
+ else:
+ # Physics-aware normalization schemes
+ y_all = physics_aware_transform(
+ E_t, nu_t, rho_t, normalization_type, nu_min, nu_max
+ )
+ dataset_all = TensorDataset(y_all)
+
+ n_total = len(dataset_all)
+ n_train = int(0.9 * n_total)
+ n_val = n_total - n_train
+ train_ds, val_ds = random_split(dataset_all, [n_train, n_val])
+
+ # Store normalization parameters for inverse transform
+ norm_params = {
+ "E_min": E_min,
+ "E_max": E_max,
+ "nu_min": nu_min,
+ "nu_max": nu_max,
+ "rho_min": rho_min,
+ "rho_max": rho_max,
+ "normalization_type": normalization_type,
+ }
+
+ # Save normalization parameters to JSON file
+ if accelerator.is_main_process:
+ norm_params_path = project_dir / "normalization_params.json"
+
+ # Convert tensors to Python floats for JSON serialization
+ norm_params_json = {
+ "E_min": float(E_min),
+ "E_max": float(E_max),
+ "nu_min": float(nu_min),
+ "nu_max": float(nu_max),
+ "rho_min": float(rho_min),
+ "rho_max": float(rho_max),
+ "normalization_type": normalization_type,
+ "dataset_size": n_total,
+ "train_size": n_train,
+ "val_size": n_val,
+ }
+
+ # Add mu/std if using standard normalization
+ if normalization_type == "standard":
+ norm_params_json.update(
+ {
+ "mu": [float(x) for x in mu.tolist()],
+ "std": [float(x) for x in std.tolist()],
+ }
+ )
+
+ with open(norm_params_path, "w") as f:
+ json.dump(norm_params_json, f, indent=2)
+
+ logger.info(f"Saved normalization parameters to {norm_params_path}")
+
+ # For compatibility with existing code, create dummy mu/std
+ mu = torch.zeros(3)
+ std = torch.ones(3)
+
+ # Wrap normalization parameters for checkpointing
+ if normalization_type == "standard":
+ mu_state = TensorState(mu.to(accelerator.device), "mu")
+ std_state = TensorState(std.to(accelerator.device), "std")
+ else:
+ # Store all normalization parameters
+ mu_state = TensorState(
+ torch.tensor([E_min, nu_min, rho_min]).to(accelerator.device), "mins"
+ )
+ std_state = TensorState(
+ torch.tensor([E_max, nu_max, rho_max]).to(accelerator.device), "maxs"
+ )
+
+ # Log initial statistics
+ if not is_dry:
+ if normalization_type == "standard":
+ accelerator.log(
+ {f"data/mu_{i}": v.item() for i, v in enumerate(mu_state.tensor)},
+ step=0,
+ )
+ accelerator.log(
+ {f"data/std_{i}": v.item() for i, v in enumerate(std_state.tensor)},
+ step=0,
+ )
+ else:
+ accelerator.log(
+ {f"data/min_{i}": v.item() for i, v in enumerate(mu_state.tensor)},
+ step=0,
+ )
+ accelerator.log(
+ {f"data/max_{i}": v.item() for i, v in enumerate(std_state.tensor)},
+ step=0,
+ )
+ accelerator.log({"data/normalization_type": normalization_type}, step=0)
+
+ # Create data loaders
+ train_loader = DataLoader(
+ train_ds,
+ batch_size=config["dataloader"]["batch_size"],
+ shuffle=True,
+ drop_last=False,
+ num_workers=config["dataloader"]["num_workers"],
+ pin_memory=config["dataloader"]["pin_memory"],
+ prefetch_factor=config["dataloader"]["prefetch_factor"],
+ persistent_workers=(
+ config["dataloader"]["persistent_workers"]
+ if config["dataloader"]["num_workers"] > 0
+ else False
+ ),
+ )
+ val_loader = DataLoader(
+ val_ds,
+ batch_size=config["dataloader"]["batch_size"],
+ shuffle=False,
+ drop_last=False,
+ num_workers=config["dataloader"]["num_workers"],
+ pin_memory=config["dataloader"]["pin_memory"],
+ prefetch_factor=config["dataloader"]["prefetch_factor"],
+ persistent_workers=(
+ config["dataloader"]["persistent_workers"]
+ if config["dataloader"]["num_workers"] > 0
+ else False
+ ),
+ )
+
+ # Choose and create model
+ use_standard_vae = config.get("standard_vae", False)
+ if use_standard_vae:
+ model = StandardVAE(**config["model"])
+ else:
+ model = TripletVAE(**config["model"])
+
+ # Store z_dim before model gets wrapped
+ z_dim = model.z_dim
+
+ # Log model information
+ if accelerator.is_main_process:
+ logger.info("Model hyper-parameters:")
+ for k, v in config["model"].items():
+ logger.info(f" {k}: {v}")
+
+ if use_standard_vae:
+ logger.info("Using StandardVAE model")
+ else:
+ logger.info("Using TripletVAE model with beta-TC and radial flow")
+
+ total_params = sum(p.numel() for p in model.parameters()) / 1e6
+ logger.info(f"Total parameters: {total_params:.2f} M")
+
+ # Log torch.compile status
+ if config.get("compile", {}).get("enabled", False):
+ compile_config = config.get("compile", {})
+ logger.info(f"Torch compile enabled:")
+ logger.info(f" Backend: {compile_config.get('backend', 'inductor')}")
+ logger.info(f" Mode: {compile_config.get('mode', 'default')}")
+
+ # Save model architecture summaries
+ enc_path = project_dir / "encoder_summary.txt"
+ dec_path = project_dir / "decoder_summary.txt"
+
+ enc_str = str(nn.Sequential(model.enc_in, model.encoder))
+ dec_str = str(nn.Sequential(model.dec_in, model.decoder))
+
+ enc_path.write_text(enc_str)
+ dec_path.write_text(dec_str)
+
+ logger.info(f"Saved encoder summary โ {enc_path}")
+ logger.info(f"Saved decoder summary โ {dec_path}")
+
+ # Create optimizer
+ optimizer = torch.optim.AdamW(
+ model.parameters(),
+ lr=config["optimizer"]["lr"],
+ weight_decay=config["optimizer"]["weight_decay"],
+ )
+
+ # Create learning rate scheduler if configured
+ lr_scheduler = None
+ if "lr_scheduler" in config and config["lr_scheduler"] is not None:
+ scheduler_config = config["lr_scheduler"]
+ scheduler_type = scheduler_config.get("type", "cosine")
+
+ if scheduler_type == "cosine":
+ lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
+ optimizer,
+ T_max=config["epochs"] * len(train_loader),
+ eta_min=scheduler_config.get("eta_min", 0),
+ )
+ elif scheduler_type == "step":
+ lr_scheduler = torch.optim.lr_scheduler.StepLR(
+ optimizer,
+ step_size=scheduler_config.get("step_size", 10),
+ gamma=scheduler_config.get("gamma", 0.1),
+ )
+ elif scheduler_type == "exponential":
+ lr_scheduler = torch.optim.lr_scheduler.ExponentialLR(
+ optimizer, gamma=scheduler_config.get("gamma", 0.95)
+ )
+
+ # Prepare everything with accelerator
+ if lr_scheduler is not None:
+ model, optimizer, train_loader, val_loader, lr_scheduler = accelerator.prepare(
+ model, optimizer, train_loader, val_loader, lr_scheduler
+ )
+ else:
+ model, optimizer, train_loader, val_loader = accelerator.prepare(
+ model, optimizer, train_loader, val_loader
+ )
+
+ # Register additional state for checkpointing
+ accelerator.register_for_checkpointing(mu_state)
+ accelerator.register_for_checkpointing(std_state)
+
+ # Create and register epoch tracker
+ epoch_tracker = EpochTracker(0)
+ accelerator.register_for_checkpointing(epoch_tracker)
+
+ # Load checkpoint if resuming
+ starting_epoch = 0
+ step_global = 0
+
+ # Check for existing checkpoints in project directory
+ resume_from_checkpoint = config.get("resume_from_checkpoint", None)
+
+ if resume_from_checkpoint is None:
+ # Look for existing checkpoints in the checkpoints subdirectory
+ checkpoints_dir = project_dir / "checkpoints"
+ if checkpoints_dir.exists():
+ checkpoints = list(checkpoints_dir.glob("checkpoint_*"))
+ if checkpoints:
+ # Sort checkpoints by number (Accelerate uses checkpoint_0, checkpoint_1, etc.)
+ def get_checkpoint_number(path):
+ try:
+ return int(path.name.split("_")[1])
+ except:
+ return -1
+
+ checkpoints = sorted(
+ checkpoints, key=get_checkpoint_number, reverse=True
+ )
+
+ # Try to find the latest valid checkpoint
+ for checkpoint in checkpoints:
+ try:
+ # Check if checkpoint is valid by looking for required files
+ if (checkpoint / "model.safetensors").exists() or (
+ checkpoint / "pytorch_model.bin"
+ ).exists():
+ resume_from_checkpoint = str(checkpoint)
+ if accelerator.is_main_process:
+ logger.info(f"Found existing checkpoint: {checkpoint}")
+ break
+ except Exception as e:
+ if accelerator.is_main_process:
+ logger.warning(
+ f"Skipping potentially corrupted checkpoint {checkpoint}: {e}"
+ )
+
+ if resume_from_checkpoint is None and checkpoints:
+ if accelerator.is_main_process:
+ logger.warning(
+ "All checkpoints appear to be corrupted, starting fresh"
+ )
+
+ if resume_from_checkpoint:
+ try:
+ accelerator.load_state(resume_from_checkpoint)
+ # After loading state, the epoch_tracker will have the correct epoch
+ starting_epoch = epoch_tracker.epoch
+ step_global = starting_epoch * len(train_loader)
+
+ # Ensure tensor states are on the correct device after loading
+ mu_state.to(accelerator.device)
+ std_state.to(accelerator.device)
+
+ # Update accelerator's iteration counter to match the epoch
+ # This ensures checkpoint naming continues correctly
+ if hasattr(accelerator, "project_configuration"):
+ # Extract checkpoint number from the loaded checkpoint path
+ checkpoint_path = Path(resume_from_checkpoint)
+ if checkpoint_path.name.startswith("checkpoint_"):
+ current_checkpoint_num = int(checkpoint_path.name.split("_")[1])
+ # Set iteration to the next checkpoint number
+ accelerator.project_configuration.iteration = (
+ current_checkpoint_num + 1
+ )
+
+ if accelerator.is_main_process:
+ logger.info(
+ f"Loaded checkpoint_{current_checkpoint_num}, next will be checkpoint_{accelerator.project_configuration.iteration}"
+ )
+
+ if accelerator.is_main_process:
+ logger.info(
+ f"Resumed from checkpoint: {resume_from_checkpoint}, continuing from epoch {starting_epoch + 1}"
+ )
+ except Exception as e:
+ if accelerator.is_main_process:
+ logger.error(f"Failed to load checkpoint {resume_from_checkpoint}: {e}")
+ logger.info("Starting training from scratch")
+ starting_epoch = 0
+ step_global = 0
+ epoch_tracker.epoch = 0
+
+ # Training loop
+ for epoch in range(starting_epoch, config["epochs"]):
+ model.train()
+
+ # Standard iteration over DataLoader
+ steps_per_epoch = len(train_loader)
+
+ prog_bar = tqdm(
+ train_loader,
+ disable=not accelerator.is_main_process,
+ desc=f"Epoch {epoch+1}/{config['epochs']}",
+ leave=False,
+ )
+
+ for step, batch in enumerate(prog_bar):
+ if batch is None:
+ continue
+ x_norm = batch[0] if isinstance(batch, (tuple, list)) else batch
+
+ with accelerator.accumulate(model):
+ # Use autocast for mixed precision
+ with accelerator.autocast():
+ recon, kl_total, details = model(x_norm)
+
+ # Different loss calculation based on model type
+ if use_standard_vae:
+ # Standard VAE loss: reconstruction + KL
+ # Apply free nats to prevent posterior collapse
+ kl_clamped = torch.clamp(kl_total, min=config["free_nats"])
+
+ # KL weight determination
+ if "kl_weight" in config:
+ # Use fixed KL weight from config
+ kl_weight = config["kl_weight"]
+ elif config.get("kl_annealing", False):
+ # Optional: KL annealing
+ kl_weight = min(
+ 1.0,
+ (epoch + 1) / config.get("kl_annealing_epochs", 100),
+ )
+ else:
+ kl_weight = 1.0
+
+ # Optional: reconstruction loss scaling
+ recon_scale = config.get("recon_scale", 1.0)
+
+ loss = recon_scale * recon + kl_weight * kl_clamped
+ else:
+ # TripletVAE with beta-TC loss
+ z = details["z"]
+ mu_post = details["mu"]
+ logvar_post = details["logvar"]
+ log_q_zx = details["log_q"]
+
+ mi, tc, kl_dim = compute_tc_terms(
+ z, mu_post, logvar_post, log_q_zx
+ )
+ kl_dim = torch.clamp(kl_dim, min=config["free_nats"] * z_dim)
+ loss = (
+ recon
+ + config["gamma"] * mi
+ + config["beta"] * tc
+ + config["alpha"] * kl_dim
+ )
+
+ accelerator.backward(loss)
+
+ if accelerator.sync_gradients:
+ accelerator.clip_grad_norm_(
+ model.parameters(), config["optimizer"]["grad_clip_norm"]
+ )
+
+ optimizer.step()
+ if lr_scheduler is not None and not isinstance(
+ lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau
+ ):
+ lr_scheduler.step()
+ optimizer.zero_grad()
+
+ # Log metrics
+ if accelerator.sync_gradients and not is_dry:
+ if use_standard_vae:
+ log_dict = {
+ "train/loss": loss.item(),
+ "train/recon": recon.item(),
+ "train/kl": kl_total.item(),
+ "train/kl_clamped": (
+ kl_clamped.item()
+ if "kl_clamped" in locals()
+ else kl_total.item()
+ ),
+ "train/lr": optimizer.param_groups[0]["lr"],
+ }
+ # Add detailed loss components if available
+ if "mse_loss" in details:
+ log_dict.update(
+ {
+ "train/mse_loss": details["mse_loss"].item(),
+ "train/nll_loss": details["nll_loss"].item(),
+ "train/std_E": details["E_std_mean"].item(),
+ "train/std_nu": details["nu_std_mean"].item(),
+ "train/std_rho": details["rho_std_mean"].item(),
+ }
+ )
+ if config.get("kl_annealing", False):
+ log_dict["train/kl_weight"] = kl_weight
+ accelerator.log(log_dict, step=step_global)
+ else:
+ log_dict = {
+ "train/loss": loss.item(),
+ "train/recon": recon.item(),
+ "train/mi": mi.item(),
+ "train/tc": tc.item(),
+ "train/kl_dim": kl_dim.item(),
+ "train/lr": optimizer.param_groups[0]["lr"],
+ }
+ # Add detailed loss components if available
+ if "mse_loss" in details:
+ log_dict.update(
+ {
+ "train/mse_loss": details["mse_loss"].item(),
+ "train/nll_loss": details["nll_loss"].item(),
+ "train/std_E": details["E_std_mean"].item(),
+ "train/std_nu": details["nu_std_mean"].item(),
+ "train/std_rho": details["rho_std_mean"].item(),
+ }
+ )
+ accelerator.log(log_dict, step=step_global)
+
+ step_global += 1
+
+ if is_dry and step_global >= 5:
+ break
+
+ if is_dry:
+ break
+
+ # Evaluation phase
+ if not is_dry and (epoch + 1) % config.get("eval_interval", 1) == 0:
+ model.eval()
+
+ # Wait for all processes before evaluation
+ accelerator.wait_for_everyone()
+
+ # Unwrap model once for evaluation
+ eval_model = accelerator.unwrap_model(model)
+
+ # Calculate validation metrics
+ with torch.no_grad():
+ # IWAE NLL
+ nll_val = iwae_nll(eval_model, val_loader, K=config.get("iwae_K", 50))
+
+ # Gather NLL from all processes
+ nll_val_tensor = torch.tensor(nll_val, device=accelerator.device)
+ nll_val_gathered = accelerator.gather(nll_val_tensor)
+ if accelerator.is_main_process and not is_dry:
+ nll_val_avg = nll_val_gathered.mean().item()
+ accelerator.log({"val/iwae_nll": nll_val_avg}, step=step_global)
+
+ # Compute latent space statistics
+ val_latent_stats = compute_latent_statistics(eval_model, val_loader)
+ if not is_dry:
+ accelerator.log(
+ {f"val/latent/{k}": v for k, v in val_latent_stats.items()},
+ step=step_global,
+ )
+
+ # Compute reconstruction metrics
+ for split_name, loader in [
+ ("train", train_loader),
+ ("val", val_loader),
+ ]:
+ errs: List[Dict[str, float]] = []
+ for batch in loader:
+ x_norm = batch[0] if isinstance(batch, (tuple, list)) else batch
+ # Deterministic reconstruction (use posterior means)
+ z_det, _, _ = eval_model.encode(x_norm, sample=False)
+ (E_mu, _), (nu_mu, _), (rho_mu, _) = eval_model.decode(z_det)
+ x_recon_norm = torch.stack(
+ [E_mu.squeeze(-1), nu_mu.squeeze(-1), rho_mu.squeeze(-1)],
+ dim=-1,
+ )
+
+ # Use appropriate inverse transform based on normalization type
+ if normalization_type == "standard":
+ x_phys = inverse_transform(
+ x_norm,
+ mu_state.tensor,
+ std_state.tensor,
+ nu_min,
+ nu_max,
+ )
+ x_recon_phys = inverse_transform(
+ x_recon_norm,
+ mu_state.tensor,
+ std_state.tensor,
+ nu_min,
+ nu_max,
+ )
+ else:
+ x_phys = physics_aware_inverse_transform(
+ x_norm,
+ E_min,
+ E_max,
+ nu_min,
+ nu_max,
+ rho_min,
+ rho_max,
+ normalization_type,
+ )
+ x_recon_phys = physics_aware_inverse_transform(
+ x_recon_norm,
+ E_min,
+ E_max,
+ nu_min,
+ nu_max,
+ rho_min,
+ rho_max,
+ normalization_type,
+ )
+
+ errs.append(reconstruction_metrics(x_phys, x_recon_phys))
+
+ # Aggregate metrics
+ agg = {k: sum(d[k] for d in errs) / len(errs) for k in errs[0]}
+
+ # Gather metrics from all processes
+ for k, v in agg.items():
+ metric_tensor = torch.tensor(v, device=accelerator.device)
+ metric_gathered = accelerator.gather(metric_tensor)
+ if accelerator.is_main_process and not is_dry:
+ metric_avg = metric_gathered.mean().item()
+ accelerator.log(
+ {f"{split_name}/{k}": metric_avg}, step=step_global
+ )
+
+ # Save checkpoint using Accelerate's save_state
+ if (epoch + 1) % config.get("save_interval", 1) == 0:
+ accelerator.wait_for_everyone()
+ if accelerator.is_main_process:
+ logger.info(f"Saving checkpoint at epoch {epoch + 1}")
+ accelerator.save_state()
+
+ # Increment epoch tracker at the end of the epoch
+ epoch_tracker.increment()
+
+ # Final checkpoint and cleanup
+ accelerator.wait_for_everyone()
+ if not is_dry:
+ if accelerator.is_main_process:
+ logger.info("Saving final checkpoint")
+ accelerator.save_state()
+
+ # Save final model in standard format
+ if accelerator.is_main_process:
+ unwrapped_model = accelerator.unwrap_model(model)
+ final_model_path = project_dir / "final_model"
+ accelerator.save_model(
+ unwrapped_model, final_model_path, safe_serialization=True
+ )
+ logger.info(f"Saved final model to {final_model_path}")
+
+ accelerator.end_training()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--config", type=str, required=True, help="Path to JSON config."
+ )
+ parser.add_argument(
+ "--lr", type=float, default=None, help="Override learning rate from config"
+ )
+ parser.add_argument(
+ "--project_dir",
+ type=str,
+ default=None,
+ help="Override project directory from config",
+ )
+ parser.add_argument(
+ "--kl_weight",
+ type=float,
+ default=None,
+ help="Override KL divergence weight (disables KL annealing)",
+ )
+ args = parser.parse_args()
+ cfg = json.loads(Path(args.config).read_text())
+
+ # Apply command-line overrides
+ if args.lr is not None:
+ cfg["optimizer"]["lr"] = args.lr
+ # Disable learning rate scheduler when LR is explicitly set
+ cfg["lr_scheduler"] = None
+ print(f"Using constant learning rate: {args.lr} (scheduler disabled)")
+
+ if args.project_dir is not None:
+ cfg["project_dir"] = args.project_dir
+
+ if args.kl_weight is not None:
+ cfg["kl_weight"] = args.kl_weight
+ cfg["kl_annealing"] = False # Disable KL annealing when weight is manually set
+
+ train(cfg)
diff --git a/deps/vomp/vomp/__init__.py b/deps/vomp/vomp/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/datasets/__init__.py b/deps/vomp/vomp/datasets/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f810caef797642a7db983c902ba3e55b8ff46419
--- /dev/null
+++ b/deps/vomp/vomp/datasets/__init__.py
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .sparse_voxel_materials import SparseVoxelMaterials
+from .components import StandardDatasetBase
+
+__all__ = ["SparseVoxelMaterials", "StandardDatasetBase"]
diff --git a/deps/vomp/vomp/datasets/components.py b/deps/vomp/vomp/datasets/components.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c8acb768b2ac6aac3c09505610f7c9ce64fbd1f
--- /dev/null
+++ b/deps/vomp/vomp/datasets/components.py
@@ -0,0 +1,180 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+from abc import abstractmethod
+import os
+import json
+import torch
+import numpy as np
+import pandas as pd
+from PIL import Image
+from torch.utils.data import Dataset
+
+
+class StandardDatasetBase(Dataset):
+ """
+ Base class for standard datasets.
+
+ Args:
+ roots (str): paths to the dataset
+ """
+
+ def __init__(
+ self,
+ roots: str,
+ ):
+ super().__init__()
+ self.roots = roots.split(",")
+ self.instances = []
+ self.metadata = pd.DataFrame()
+
+ self._stats = {}
+ for root in self.roots:
+ key = os.path.basename(root)
+ self._stats[key] = {}
+ metadata = pd.read_csv(os.path.join(root, "metadata.csv"))
+ self._stats[key]["Total"] = len(metadata)
+ metadata, stats = self.filter_metadata(metadata)
+ self._stats[key].update(stats)
+ self.instances.extend(
+ [(root, sha256) for sha256 in metadata["sha256"].values]
+ )
+ metadata.set_index("sha256", inplace=True)
+ self.metadata = pd.concat([self.metadata, metadata])
+
+ @abstractmethod
+ def filter_metadata(
+ self, metadata: pd.DataFrame
+ ) -> Tuple[pd.DataFrame, Dict[str, int]]:
+ pass
+
+ @abstractmethod
+ def get_instance(self, root: str, instance: str) -> Dict[str, Any]:
+ pass
+
+ def __len__(self):
+ return len(self.instances)
+
+ def __getitem__(self, index) -> Dict[str, Any]:
+ return self._get_item_with_retry(index, max_retries=5)
+
+ def _get_item_with_retry(
+ self, index, max_retries=5, current_retry=0
+ ) -> Dict[str, Any]:
+ """Get item with limited retry attempts to prevent infinite recursion."""
+ try:
+ root, instance = self.instances[index]
+ return self.get_instance(root, instance)
+ except Exception as e:
+ if current_retry >= max_retries:
+ raise RuntimeError(
+ f"Failed to get item after {max_retries} retries: {e}"
+ )
+
+ print(
+ f"Error getting item {index}, retrying ({current_retry+1}/{max_retries}): {e}"
+ )
+ new_index = np.random.randint(0, len(self))
+ return self._get_item_with_retry(new_index, max_retries, current_retry + 1)
+
+ def __str__(self):
+ lines = []
+ lines.append(self.__class__.__name__)
+ lines.append(f" - Total instances: {len(self)}")
+ lines.append(f" - Sources:")
+ for key, stats in self._stats.items():
+ lines.append(f" - {key}:")
+ for k, v in stats.items():
+ lines.append(f" - {k}: {v}")
+ return "\n".join(lines)
+
+
+class TextConditionedMixin:
+ def __init__(self, roots, **kwargs):
+ super().__init__(roots, **kwargs)
+ self.captions = {}
+ for instance in self.instances:
+ sha256 = instance[1]
+ self.captions[sha256] = json.loads(self.metadata.loc[sha256]["captions"])
+
+ def filter_metadata(self, metadata):
+ metadata, stats = super().filter_metadata(metadata)
+ metadata = metadata[metadata["captions"].notna()]
+ stats["With captions"] = len(metadata)
+ return metadata, stats
+
+ def get_instance(self, root, instance):
+ pack = super().get_instance(root, instance)
+ text = np.random.choice(self.captions[instance])
+ pack["cond"] = text
+ return pack
+
+
+class ImageConditionedMixin:
+ def __init__(self, roots, *, image_size=518, **kwargs):
+ self.image_size = image_size
+ super().__init__(roots, **kwargs)
+
+ def filter_metadata(self, metadata):
+ metadata, stats = super().filter_metadata(metadata)
+ metadata = metadata[metadata[f"cond_rendered"]]
+ stats["Cond rendered"] = len(metadata)
+ return metadata, stats
+
+ def get_instance(self, root, instance):
+ pack = super().get_instance(root, instance)
+
+ image_root = os.path.join(root, "renders_cond", instance)
+ with open(os.path.join(image_root, "transforms.json")) as f:
+ metadata = json.load(f)
+ n_views = len(metadata["frames"])
+ view = np.random.randint(n_views)
+ metadata = metadata["frames"][view]
+
+ image_path = os.path.join(image_root, metadata["file_path"])
+ image = Image.open(image_path)
+
+ alpha = np.array(image.getchannel(3))
+ bbox = np.array(alpha).nonzero()
+ bbox = [bbox[1].min(), bbox[0].min(), bbox[1].max(), bbox[0].max()]
+ center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
+ hsize = max(bbox[2] - bbox[0], bbox[3] - bbox[1]) / 2
+ aug_size_ratio = 1.2
+ aug_hsize = hsize * aug_size_ratio
+ aug_center_offset = [0, 0]
+ aug_center = [
+ center[0] + aug_center_offset[0],
+ center[1] + aug_center_offset[1],
+ ]
+ aug_bbox = [
+ int(aug_center[0] - aug_hsize),
+ int(aug_center[1] - aug_hsize),
+ int(aug_center[0] + aug_hsize),
+ int(aug_center[1] + aug_hsize),
+ ]
+ image = image.crop(aug_bbox)
+
+ image = image.resize(
+ (self.image_size, self.image_size), Image.Resampling.LANCZOS
+ )
+ alpha = image.getchannel(3)
+ image = image.convert("RGB")
+ image = torch.tensor(np.array(image)).permute(2, 0, 1).float() / 255.0
+ alpha = torch.tensor(np.array(alpha)).float() / 255.0
+ image = image * alpha.unsqueeze(0)
+ pack["cond"] = image
+
+ return pack
diff --git a/deps/vomp/vomp/datasets/sparse_voxel_materials.py b/deps/vomp/vomp/datasets/sparse_voxel_materials.py
new file mode 100644
index 0000000000000000000000000000000000000000..43be5c11d86863a6546af98b1414108559feab1e
--- /dev/null
+++ b/deps/vomp/vomp/datasets/sparse_voxel_materials.py
@@ -0,0 +1,437 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from PIL import Image
+import json
+import numpy as np
+import pandas as pd
+import torch
+import utils3d.torch
+from ..modules.sparse.basic import SparseTensor
+from ..utils.material_transforms import MaterialPropertyTransform
+from .components import StandardDatasetBase
+
+
+class SparseVoxelMaterials(StandardDatasetBase):
+ """
+ SparseVoxelMaterials dataset.
+
+ Args:
+ roots (str): paths to the dataset
+ image_size (int): size of the image
+ model (str): model name
+ resolution (int): resolution of the data
+ min_aesthetic_score (float): minimum aesthetic score
+ max_num_voxels (int): maximum number of voxels
+ compute_material_stats (bool): whether to recompute material statistics
+ split (str): split filter
+ normalization_type (str): type of normalization ("standard" or "log_minmax")
+ """
+
+ def __init__(
+ self,
+ roots: str,
+ image_size: int,
+ model: str = "dinov2_vitl14_reg",
+ resolution: int = 64,
+ min_aesthetic_score: float = 5.0,
+ max_num_voxels: int = 32768,
+ compute_material_stats: bool = False,
+ split: str = None,
+ normalization_type: str = "log_minmax",
+ normalization_params_file: str = None, # Add parameter to load normalization params
+ ):
+ self.image_size = image_size
+ self.model = model
+ self.resolution = resolution
+ self.min_aesthetic_score = min_aesthetic_score
+ self.max_num_voxels = max_num_voxels
+ self.split = split
+ self.value_range = (0, 1)
+ self.normalization_type = normalization_type
+
+ self.material_transform = MaterialPropertyTransform(
+ normalization_type=normalization_type
+ )
+
+ # Load normalization parameters from file (optional for direct prediction)
+ if normalization_params_file is not None:
+ if not os.path.exists(normalization_params_file):
+ raise FileNotFoundError(
+ f"Normalization parameters file not found: {normalization_params_file}"
+ )
+ print(f"Loading normalization parameters from: {normalization_params_file}")
+ self._load_normalization_params(normalization_params_file)
+ else:
+ print(
+ "No normalization parameters file provided - using dataset-derived normalization for direct prediction"
+ )
+
+ super().__init__(roots)
+
+ if compute_material_stats and normalization_type == "standard":
+ print(
+ "Recomputing material transform statistics for standard normalization..."
+ )
+ self._compute_material_stats()
+
+ def _load_normalization_params(self, params_file: str):
+ """Load normalization parameters from JSON file (saved during matvae training)."""
+ import json
+
+ with open(params_file, "r") as f:
+ params = json.load(f)
+
+ # Validate normalization type consistency
+ if params.get("normalization_type") != self.normalization_type:
+ raise ValueError(
+ f"Normalization type mismatch! "
+ f"Dataset config: {self.normalization_type}, "
+ f"Params file: {params.get('normalization_type')}"
+ )
+
+ if self.normalization_type == "standard":
+ if "mu" not in params or "std" not in params:
+ raise ValueError(
+ "Standard normalization requires 'mu' and 'std' in params file"
+ )
+
+ self.material_transform.mu = torch.tensor(params["mu"])
+ self.material_transform.std = torch.tensor(params["std"])
+ self.material_transform.nu_min = params.get("nu_min", 0.0)
+ self.material_transform.nu_max = params.get("nu_max", 0.5)
+ self.material_transform._stats_computed = True
+
+ print(f"Loaded standard normalization params:")
+ print(f" mu: {self.material_transform.mu}")
+ print(f" std: {self.material_transform.std}")
+ print(f" nu_min: {self.material_transform.nu_min}")
+ print(f" nu_max: {self.material_transform.nu_max}")
+
+ elif self.normalization_type == "log_minmax":
+ required_keys = ["E_min", "E_max", "nu_min", "nu_max", "rho_min", "rho_max"]
+ for key in required_keys:
+ if key not in params:
+ raise ValueError(
+ f"Log minmax normalization requires '{key}' in params file"
+ )
+
+ # Convert raw values to log space (params file stores raw values, but transform expects log values)
+ import math
+
+ self.material_transform.E_min = math.log10(params["E_min"])
+ self.material_transform.E_max = math.log10(params["E_max"])
+ self.material_transform.nu_min = params["nu_min"]
+ self.material_transform.nu_max = params["nu_max"]
+ self.material_transform.rho_min = math.log10(params["rho_min"])
+ self.material_transform.rho_max = math.log10(params["rho_max"])
+ self.material_transform._stats_computed = True
+
+ print(f"Loaded log minmax normalization params:")
+ print(
+ f" E_min: {self.material_transform.E_min:.6f} (log10({params['E_min']:.0f} Pa))"
+ )
+ print(
+ f" E_max: {self.material_transform.E_max:.6f} (log10({params['E_max']:.0f} Pa))"
+ )
+ print(f" nu_min: {self.material_transform.nu_min:.6f}")
+ print(f" nu_max: {self.material_transform.nu_max:.6f}")
+ print(
+ f" rho_min: {self.material_transform.rho_min:.6f} (log10({params['rho_min']:.2f} kg/mยณ))"
+ )
+ print(
+ f" rho_max: {self.material_transform.rho_max:.6f} (log10({params['rho_max']:.2f} kg/mยณ))"
+ )
+
+ elif self.normalization_type == "log_minmax_no_density":
+ required_keys = ["E_min", "E_max", "nu_min", "nu_max", "rho_min", "rho_max"]
+ for key in required_keys:
+ if key not in params:
+ raise ValueError(
+ f"Log minmax no density normalization requires '{key}' in params file"
+ )
+
+ # Convert E values to log space, keep density in raw space
+ import math
+
+ self.material_transform.E_min = math.log10(params["E_min"])
+ self.material_transform.E_max = math.log10(params["E_max"])
+ self.material_transform.nu_min = params["nu_min"]
+ self.material_transform.nu_max = params["nu_max"]
+ self.material_transform.rho_min = params[
+ "rho_min"
+ ] # Keep raw density values
+ self.material_transform.rho_max = params[
+ "rho_max"
+ ] # Keep raw density values
+ self.material_transform._stats_computed = True
+
+ print(f"Loaded log minmax no density normalization params:")
+ print(
+ f" E_min: {self.material_transform.E_min:.6f} (log10({params['E_min']:.0f} Pa))"
+ )
+ print(
+ f" E_max: {self.material_transform.E_max:.6f} (log10({params['E_max']:.0f} Pa))"
+ )
+ print(f" nu_min: {self.material_transform.nu_min:.6f}")
+ print(f" nu_max: {self.material_transform.nu_max:.6f}")
+ print(f" rho_min: {self.material_transform.rho_min:.2f} (kg/mยณ)")
+ print(f" rho_max: {self.material_transform.rho_max:.2f} (kg/mยณ)")
+ else:
+ raise ValueError(f"Unknown normalization_type: {self.normalization_type}")
+
+ def _compute_material_stats(self):
+ """Compute statistics for material property transforms (standard normalization only)."""
+ if self.normalization_type != "standard":
+ print(
+ f"Warning: _compute_material_stats() called with normalization_type='{self.normalization_type}'. This method only works with 'standard' normalization."
+ )
+ return
+
+ print("Computing material transform statistics for standard normalization...")
+
+ # Create a simple dataloader for computing stats
+ from torch.utils.data import DataLoader
+
+ # Use a smaller batch size for stats computation to avoid memory issues
+ stats_loader = DataLoader(
+ self,
+ batch_size=1,
+ shuffle=False,
+ collate_fn=self._stats_collate_fn,
+ num_workers=0, # Single-threaded for stats computation
+ )
+
+ # Compute stats using the transform class
+ mu, std = self.material_transform.compute_stats(stats_loader)
+ print(
+ f"Material transform stats computed: ฮผ={self.material_transform.mu}, ฯ={self.material_transform.std}"
+ )
+
+ @staticmethod
+ def _stats_collate_fn(batch):
+ """Special collate function for computing material statistics."""
+ # Extract raw materials from batch
+ all_materials = []
+ for sample in batch:
+ materials = sample["materials"] # Shape: (num_voxels, 3)
+ all_materials.append(materials)
+
+ all_materials = torch.cat(all_materials, dim=0) # Shape: (total_voxels, 3)
+
+ # Convert to dictionary format expected by MaterialPropertyTransform
+ material_properties = {
+ "youngs_modulus": all_materials[:, 0],
+ "poissons_ratio": all_materials[:, 1],
+ "density": all_materials[:, 2],
+ }
+
+ return {"material_properties": material_properties}
+
+ def filter_metadata(self, metadata):
+ stats = {}
+ metadata = metadata[metadata[f"feature_{self.model}"]]
+ stats["With features"] = len(metadata)
+
+ if self.split is not None:
+ if "split" in metadata.columns:
+ metadata = metadata[metadata["split"] == self.split]
+ stats[f"Split '{self.split}'"] = len(metadata)
+ else:
+ print(
+ f"Warning: 'split' column not found in metadata, ignoring split filter"
+ )
+
+ # metadata = metadata[metadata["aesthetic_score"] >= self.min_aesthetic_score]
+ # stats[f"Aesthetic score >= {self.min_aesthetic_score}"] = len(metadata)
+ return metadata, stats
+
+ def _get_image(self, root, instance):
+ with open(os.path.join(root, "renders", instance, "transforms.json")) as f:
+ metadata = json.load(f)
+ n_views = len(metadata["frames"])
+ view = np.random.randint(n_views)
+ metadata = metadata["frames"][view]
+ fov = metadata["camera_angle_x"]
+ intrinsics = utils3d.torch.intrinsics_from_fov_xy(
+ torch.tensor(fov), torch.tensor(fov)
+ )
+ c2w = torch.tensor(metadata["transform_matrix"])
+ c2w[:3, 1:3] *= -1
+ extrinsics = torch.inverse(c2w)
+
+ image_path = os.path.join(root, "renders", instance, metadata["file_path"])
+ image = Image.open(image_path)
+ alpha = image.getchannel(3)
+ image = image.convert("RGB")
+ image = image.resize(
+ (self.image_size, self.image_size), Image.Resampling.LANCZOS
+ )
+ alpha = alpha.resize(
+ (self.image_size, self.image_size), Image.Resampling.LANCZOS
+ )
+ image = torch.tensor(np.array(image)).permute(2, 0, 1).float() / 255.0
+ alpha = torch.tensor(np.array(alpha)).float() / 255.0
+
+ return {
+ "image": image,
+ "alpha": alpha,
+ "extrinsics": extrinsics,
+ "intrinsics": intrinsics,
+ }
+
+ def _get_feat(self, root, instance):
+ DATA_RESOLUTION = 64 # Must match the resolution used during feature extraction for this dataset
+ feats_path = os.path.join(root, "features", self.model, f"{instance}.npz")
+ feats = np.load(feats_path, allow_pickle=True)
+ coords = torch.tensor(feats["indices"]).int()
+ feats = torch.tensor(feats["patchtokens"]).float()
+
+ materials_path = os.path.join(root, "voxels", f"{instance}_with_materials.npz")
+ materials_data = np.load(materials_path, allow_pickle=True)
+ voxel_data = materials_data["voxel_data"]
+
+ # Extract material properties: [youngs_modulus, poissons_ratio, density]
+ materials = np.column_stack(
+ [
+ voxel_data["youngs_modulus"],
+ voxel_data["poissons_ratio"],
+ voxel_data["density"],
+ ]
+ )
+ materials = torch.tensor(materials).float() # Shape: (voxels, 3)
+
+ if self.resolution != DATA_RESOLUTION:
+ factor = DATA_RESOLUTION // self.resolution
+ original_coords = coords.clone()
+ original_materials = materials.clone()
+
+ coords = coords // factor
+ coords, idx = coords.unique(return_inverse=True, dim=0)
+ feats = torch.scatter_reduce(
+ torch.zeros(coords.shape[0], feats.shape[1], device=feats.device),
+ dim=0,
+ index=idx.unsqueeze(-1).expand(-1, feats.shape[1]),
+ src=feats,
+ reduce="mean",
+ )
+
+ # Nearest neighbor interpolation for materials
+ # For each downsampled voxel, find the nearest original voxel
+ downsampled_centers = (
+ coords.float() * factor + factor / 2
+ ) # Centers of downsampled voxels
+ original_coords_float = original_coords.float()
+
+ # Find nearest neighbor for each downsampled voxel
+ nearest_materials = []
+ for center in downsampled_centers:
+ # Calculate distances to all original voxels
+ distances = torch.norm(original_coords_float - center, dim=1)
+ nearest_idx = torch.argmin(distances)
+ nearest_materials.append(original_materials[nearest_idx])
+
+ materials = torch.stack(nearest_materials)
+
+ # Per-object voxel sampling: limit each object to max_num_voxels
+ num_voxels = coords.shape[0]
+ if num_voxels > self.max_num_voxels:
+ # Randomly sample max_num_voxels from this object
+ indices = torch.randperm(num_voxels)[: self.max_num_voxels]
+ coords = coords[indices]
+ feats = feats[indices]
+ materials = materials[indices]
+
+ return {
+ "coords": coords,
+ "feats": feats,
+ "materials": materials,
+ }
+
+ def collate_fn(self, batch):
+ pack = {}
+ coords = []
+ for i, b in enumerate(batch):
+ coords.append(
+ torch.cat(
+ [
+ torch.full((b["coords"].shape[0], 1), i, dtype=torch.int32),
+ b["coords"],
+ ],
+ dim=-1,
+ )
+ )
+ coords = torch.cat(coords)
+ feats = torch.cat([b["feats"] for b in batch])
+ raw_materials = torch.cat([b["materials"] for b in batch])
+
+ # Note: Per-object voxel sampling is now handled in _get_feat()
+ # No cross-batch sampling needed since each object is already limited to max_num_voxels
+
+ pack["feats"] = SparseTensor(
+ coords=coords,
+ feats=feats,
+ )
+
+ # Apply material transforms if stats have been computed
+ if self.material_transform._stats_computed:
+ # Convert to dictionary format
+ material_dict = {
+ "youngs_modulus": raw_materials[:, 0],
+ "poissons_ratio": raw_materials[:, 1],
+ "density": raw_materials[:, 2],
+ }
+
+ # Apply forward transform and standardization
+ transformed_dict = (
+ self.material_transform.forward_transform_and_standardize(material_dict)
+ )
+
+ # Convert back to tensor format
+ transformed_materials = torch.stack(
+ [
+ transformed_dict["youngs_modulus"],
+ transformed_dict["poissons_ratio"],
+ transformed_dict["density"],
+ ],
+ dim=-1,
+ )
+
+ pack["materials"] = SparseTensor(
+ coords=coords,
+ feats=transformed_materials,
+ )
+ else:
+ # If stats not computed, just store raw materials
+ pack["materials"] = SparseTensor(
+ coords=coords,
+ feats=raw_materials,
+ )
+
+ # pack["image"] = torch.stack([b["image"] for b in batch])
+ # pack["alpha"] = torch.stack([b["alpha"] for b in batch])
+ # pack["extrinsics"] = torch.stack([b["extrinsics"] for b in batch])
+ # pack["intrinsics"] = torch.stack([b["intrinsics"] for b in batch])
+
+ return pack
+
+ def get_instance(self, root, instance):
+ # image = self._get_image(root, instance)
+ feat = self._get_feat(root, instance)
+ return {
+ # **image,
+ **feat,
+ }
diff --git a/deps/vomp/vomp/fem/.gitattributes b/deps/vomp/vomp/fem/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..8c6e042adc94915aeee8244970167d470d23cc9f
--- /dev/null
+++ b/deps/vomp/vomp/fem/.gitattributes
@@ -0,0 +1,2 @@
+*.msh filter=lfs diff=lfs merge=lfs -text
+*.usd filter=lfs diff=lfs merge=lfs -text
diff --git a/deps/vomp/vomp/fem/.gitignore b/deps/vomp/vomp/fem/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ac6ab0fc18913c3fd7150b5596baf0c74519f35e
--- /dev/null
+++ b/deps/vomp/vomp/fem/.gitignore
@@ -0,0 +1,11 @@
+__pycache__
+.vscode
+*.ini
+**/outputs
+*.png
+*.usd
+*.nsys-rep
+*.pt
+*.mp4
+*.swp
+simulations/material_grids_generated.py
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/README.md b/deps/vomp/vomp/fem/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b6c36254d065e0dd244921ebc8b076b13a15469d
--- /dev/null
+++ b/deps/vomp/vomp/fem/README.md
@@ -0,0 +1,10 @@
+# Warp.fem Examples
+
+This repository contains an unstructured collection of example simulators built with `warp.fem`, plus common utilities.
+
+Most examples require the repository root directory to be in the python module path, for instance run as:
+
+```
+PYTHONPATH=. python fem_examples/model_problems/example_elastic_wave.py assets/hollow_square-res1.msh
+```
+
diff --git a/deps/vomp/vomp/fem/fem_examples/__init__.py b/deps/vomp/vomp/fem/fem_examples/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/__init__.py b/deps/vomp/vomp/fem/fem_examples/mfem/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/collisions.py b/deps/vomp/vomp/fem/fem_examples/mfem/collisions.py
new file mode 100644
index 0000000000000000000000000000000000000000..e112ddb36af4c8cb24dc6bc803f361d9c035021f
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/collisions.py
@@ -0,0 +1,659 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List, Any
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+
+import argparse
+
+from fem_examples.mfem.softbody_sim import SoftbodySim
+
+
+class CollisionHandler:
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ parser.add_argument(
+ "--collision_stiffness",
+ "-ck",
+ type=float,
+ default=1.0,
+ help="Multiplier for collision force/energy",
+ )
+ parser.add_argument(
+ "--collision_radius",
+ "-cr",
+ type=float,
+ default=0.1,
+ help="Radius of interaction for collision particles",
+ )
+ parser.add_argument(
+ "--collision_detection_ratio",
+ "-cd",
+ type=float,
+ default=2.0,
+ help="Multiplier of collision radius for detection",
+ )
+ parser.add_argument("--friction", "-mu", type=float, default=0.2)
+ parser.add_argument(
+ "--friction_reg",
+ "-mur",
+ type=float,
+ default=0.1,
+ help="Regularization coefficient for friction",
+ )
+ parser.add_argument(
+ "--friction_fluid",
+ "-nu",
+ type=float,
+ default=0.01,
+ help="Additional fluid friction ratio to convexify things",
+ )
+ parser.add_argument(
+ "--ground",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Do ground collisions",
+ )
+ parser.add_argument(
+ "--ground_height",
+ type=float,
+ default=0.0,
+ help="Ground height",
+ )
+
+ def __init__(
+ self,
+ kinematic_meshes: List[wp.Mesh],
+ cp_cell_indices,
+ cp_cell_coords,
+ sim: SoftbodySim,
+ ):
+ self.args = sim.args
+ self.sim = sim
+ self.warp_meshes = kinematic_meshes
+
+ n_cp = cp_cell_indices.shape[0]
+ collision_quadrature = fem.PicQuadrature(
+ domain=sim.vel_quadrature.domain,
+ positions=(cp_cell_indices, cp_cell_coords),
+ measures=wp.ones(n_cp, dtype=float),
+ )
+ self.set_collision_quadrature(collision_quadrature)
+
+ self.n_contact = 0
+
+ max_contacts = 10 * cp_cell_indices.shape[0]
+ self.collision_indices_a = wp.empty(max_contacts, dtype=int)
+ self.collision_indices_b = wp.empty(max_contacts, dtype=int)
+ self.collision_normals = wp.empty(max_contacts, dtype=wp.vec3)
+ self.collision_kinematic_gaps = wp.empty(max_contacts, dtype=wp.vec3)
+
+ jac_cols = sim.u_field.space_partition.node_count()
+ self._collision_jacobian_a = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+ self._collision_jacobian_b = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+
+ self._collision_jacobian = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+ self._collision_jacobian_t = sp.bsr_zeros(jac_cols, 0, block_type=wp.mat33)
+
+ self._HtH_work_arrays = sp.bsr_mm_work_arrays()
+ self._HbHa_work_arrays = sp.bsr_axpy_work_arrays()
+
+ self._collision_stiffness = (
+ self.args.collision_stiffness * self.args.density / n_cp
+ )
+ print(self._collision_stiffness)
+
+ def set_collision_quadrature(self, quadrature: fem.PicQuadrature):
+ self.collision_quadrature = quadrature
+
+ def add_collision_energy(self, E: float):
+ if self.n_contact == 0:
+ return E
+
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+ col_energies = wp.empty(self.n_contact, dtype=float)
+ wp.launch(
+ collision_energy,
+ dim=self.n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.friction,
+ self.args.dt * self.args.friction_reg,
+ self.args.friction_fluid * self.args.friction_reg,
+ cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ col_energies,
+ ],
+ )
+ return E + self._collision_stiffness * wp.utils.array_sum(col_energies)
+
+ def add_collision_hessian(self, lhs: wp.array):
+ # contacts
+ if self.n_contact == 0:
+ return lhs
+
+ H = self._collision_jacobian
+ Ht = self._collision_jacobian_t
+ wp.launch(
+ bsr_mul_diag,
+ dim=(Ht.nnz_sync(), Ht.block_shape[0]),
+ inputs=[Ht.scalar_values, Ht.columns, self._col_energy_hessian],
+ )
+
+ sp.bsr_mm(
+ x=Ht,
+ y=H,
+ z=lhs,
+ alpha=self._collision_stiffness,
+ beta=1.0,
+ work_arrays=self._HtH_work_arrays,
+ )
+
+ return lhs
+
+ def add_collision_forces(self, rhs: wp.array):
+ if self.n_contact == 0:
+ return rhs
+
+ # contacts
+ sp.bsr_mv(
+ A=self._collision_jacobian_t,
+ x=self._col_energy_gradients,
+ y=rhs,
+ alpha=-self._collision_stiffness,
+ beta=1.0,
+ )
+
+ return rhs
+
+ def prepare_newton_step(self, dt: float):
+ self.detect_collisions(dt)
+ self.build_collision_jacobian()
+
+ # compute per-contact forces and hessian
+ n_contact = self.n_contact
+ if n_contact > 0:
+ self._col_energy_gradients = wp.empty(n_contact, dtype=wp.vec3)
+ self._col_energy_hessian = wp.empty(n_contact, dtype=wp.mat33)
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+
+ wp.launch(
+ collision_gradient_and_hessian,
+ dim=n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.friction,
+ dt * self.args.friction_reg,
+ self.args.friction_fluid * self.args.friction_reg,
+ cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ self._col_energy_gradients,
+ self._col_energy_hessian,
+ ],
+ )
+
+ def cp_world_position(self, dest=None):
+ cp_pic = self.collision_quadrature
+ if dest is None:
+ dest = wp.empty(cp_pic.total_point_count(), dtype=wp.vec3)
+ fem.interpolate(
+ world_position,
+ fields={"u": self.sim.u_field},
+ dest=dest,
+ quadrature=cp_pic,
+ )
+
+ return dest
+
+ def _sample_cp_displacement(self, du_field, dest=None):
+ cp_pic = self.collision_quadrature
+ if dest is None:
+ dest = wp.empty(cp_pic.total_point_count(), dtype=wp.vec3)
+ fem.interpolate(
+ du_field,
+ dest=dest,
+ quadrature=cp_pic,
+ )
+ return dest
+
+ def detect_collisions(self, dt):
+ max_contacts = self.collision_normals.shape[0]
+
+ count = wp.zeros(1, dtype=int)
+ indices_a = self.collision_indices_a
+ indices_b = self.collision_indices_b
+ normals = self.collision_normals
+ kinematic_gaps = self.collision_kinematic_gaps
+
+ self.run_collision_detectors(
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ )
+
+ self.n_contact = int(count.numpy()[0])
+
+ if self.n_contact > max_contacts:
+ print("Warning: contact buffer size exceeded, some have bee ignored")
+ self.n_contact = max_contacts
+
+ def run_collision_detectors(
+ self,
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ ):
+ cp_pic = self.collision_quadrature
+ n_cp = cp_pic.total_point_count()
+ max_contacts = self.collision_normals.shape[0]
+
+ cp_cur_pos = self.cp_world_position()
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+
+ collision_radius = (
+ self.args.collision_radius * self.args.collision_detection_ratio
+ )
+
+ if self.args.ground:
+ ground_height = self.args.ground_height
+ wp.launch(
+ detect_ground_collisions,
+ dim=n_cp,
+ inputs=[
+ max_contacts,
+ self.args.up_axis,
+ cp_cur_pos,
+ cp_du,
+ collision_radius,
+ ground_height,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+ if self.warp_meshes:
+ mesh_ids = wp.array([mesh.id for mesh in self.warp_meshes], dtype=wp.uint64)
+ wp.launch(
+ detect_mesh_collisions,
+ dim=(len(mesh_ids), n_cp),
+ inputs=[
+ max_contacts,
+ dt,
+ mesh_ids,
+ cp_cur_pos,
+ cp_du,
+ collision_radius,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+
+ def build_collision_jacobian(self):
+ n_contact = self.n_contact
+
+ # Build collision jacobian
+ # (derivative of collision gap `pos_a - pos_b` w.r.t. degrees of freedom)
+
+ if n_contact == 0:
+ return
+
+ a_cells = wp.empty(n_contact, dtype=int)
+ a_coords = wp.empty(n_contact, dtype=wp.vec3)
+ b_cells = wp.empty(n_contact, dtype=int)
+ b_coords = wp.empty(n_contact, dtype=wp.vec3)
+ wp.launch(
+ gather_cell_coordinates,
+ dim=n_contact,
+ inputs=[
+ self.collision_quadrature.cell_indices,
+ self.collision_quadrature.particle_coords,
+ self.collision_indices_a,
+ a_cells,
+ a_coords,
+ ],
+ )
+ wp.launch(
+ gather_cell_coordinates,
+ dim=n_contact,
+ inputs=[
+ self.collision_quadrature.cell_indices,
+ self.collision_quadrature.particle_coords,
+ self.collision_indices_b,
+ b_cells,
+ b_coords,
+ ],
+ )
+
+ measures = wp.ones(n_contact, dtype=float)
+
+ a_contact_pic = fem.PicQuadrature(
+ self.collision_quadrature.domain,
+ positions=(a_cells, a_coords),
+ measures=measures,
+ )
+ b_contact_pic = fem.PicQuadrature(
+ self.collision_quadrature.domain,
+ positions=(b_cells, b_coords),
+ measures=measures,
+ )
+ u_trial = fem.make_trial(
+ self.sim.u_field.space, space_partition=self.sim.u_field.space_partition
+ )
+
+ sp.bsr_set_zero(
+ self._collision_jacobian_a,
+ n_contact,
+ self.sim.u_field.space_partition.node_count(),
+ )
+ fem.interpolate(
+ u_trial,
+ quadrature=a_contact_pic,
+ dest=self._collision_jacobian_a,
+ bsr_options={"prune_numerical_zeros": False},
+ )
+
+ sp.bsr_set_zero(
+ self._collision_jacobian_b,
+ n_contact,
+ self.sim.u_field.space_partition.node_count(),
+ )
+ fem.interpolate(
+ u_trial,
+ quadrature=b_contact_pic,
+ dest=self._collision_jacobian_b,
+ bsr_options={"prune_numerical_zeros": False},
+ )
+
+ self._collision_jacobian_a.nnz_sync()
+ self._collision_jacobian_b.nnz_sync()
+
+ sp.bsr_assign(self._collision_jacobian, src=self._collision_jacobian_a)
+ sp.bsr_axpy(
+ x=self._collision_jacobian_b,
+ y=self._collision_jacobian,
+ alpha=-1,
+ beta=1,
+ work_arrays=self._HbHa_work_arrays,
+ )
+
+ sp.bsr_set_transpose(
+ dest=self._collision_jacobian_t, src=self._collision_jacobian
+ )
+
+
+@wp.kernel
+def bsr_mul_diag(
+ Bt_values: wp.array3d(dtype=float),
+ Bt_columns: wp.array(dtype=int),
+ C_values: wp.array(dtype=Any),
+):
+ i, r = wp.tid()
+ col = Bt_columns[i]
+
+ C = C_values[col]
+
+ Btr = Bt_values[i, r]
+ BtC = wp.vec3(Btr[0], Btr[1], Btr[2]) @ C
+ for k in range(3):
+ Btr[k] = BtC[k]
+
+
+@wp.kernel
+def detect_ground_collisions(
+ max_contacts: int,
+ up_axis: int,
+ pos_cur: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ ground_height: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ i = wp.tid()
+ x = pos_cur[i]
+
+ if x[up_axis] < ground_height + radius:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ nor = fem.linalg.basis_element(wp.vec3(), up_axis)
+
+ normals[idx] = nor
+ kinematic_gaps[idx] = (wp.dot(x - du_cur[i], nor) - ground_height) * nor
+ indices_a[idx] = i
+ indices_b[idx] = fem.NULL_QP_INDEX
+
+
+@wp.kernel
+def detect_mesh_collisions(
+ max_contacts: int,
+ dt: float,
+ mesh_ids: wp.array(dtype=wp.uint64),
+ pos_cur: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ m, tid = wp.tid()
+ mesh_id = mesh_ids[m]
+
+ x = pos_cur[tid]
+
+ query = wp.mesh_query_point(mesh_id, x, radius)
+
+ if query.result:
+ cp = wp.mesh_eval_position(mesh_id, query.face, query.u, query.v)
+
+ delta = x - cp
+ dist = wp.length(delta) * query.sign
+
+ if dist < radius:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ if dist < 0.00001:
+ n = wp.mesh_eval_face_normal(mesh_id, query.face)
+ else:
+ n = wp.normalize(delta) * query.sign
+ normals[idx] = n
+
+ v = wp.mesh_eval_velocity(mesh_id, query.face, query.u, query.v)
+
+ kinematic_gap = (dist - wp.dot(du_cur[tid], n)) * n - v * dt
+ kinematic_gaps[idx] = kinematic_gap
+ indices_a[idx] = tid
+ indices_b[idx] = fem.NULL_QP_INDEX
+
+
+@wp.func
+def collision_offset(
+ c: int,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ idx_a = indices_a[c]
+ idx_b = indices_b[c]
+
+ offset = du_cur[idx_a] + kinematic_gaps[c]
+ if idx_b != fem.NULL_QP_INDEX:
+ offset -= du_cur[idx_b]
+
+ return offset
+
+
+@wp.func
+def collision_target_distance(
+ c: int,
+ radius: float,
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ return wp.where(indices_b[c] == fem.NULL_ELEMENT_INDEX, 1.0, 2.0) * radius
+
+
+@wp.kernel
+def collision_energy(
+ radius: float,
+ mu: float,
+ dt: float,
+ nu: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ energies: wp.array(dtype=float),
+):
+ c = wp.tid()
+
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+
+ nor = normals[c]
+ d = wp.dot(offset, nor)
+ d_hat = d / rc
+
+ stick = wp.where(d_hat < 1.0, 1.0, 0.0)
+ gap = d_hat - 1.0
+ E = 0.5 * stick * gap * gap
+
+ vt = (offset - d * nor) / dt # tangential velocity
+ vt_norm = wp.length(vt)
+
+ mu_fn = -mu * wp.min(0.0, gap) / rc # yield force
+
+ E += (
+ mu_fn
+ * dt
+ * (
+ 0.5 * nu * vt_norm * vt_norm
+ + wp.where(
+ vt_norm < 1.0,
+ vt_norm * vt_norm * (1.0 - vt_norm / 3.0),
+ vt_norm - 1.0 / 3.0,
+ )
+ )
+ )
+
+ energies[c] = E
+
+
+@wp.kernel
+def collision_gradient_and_hessian(
+ radius: float,
+ mu: float,
+ dt: float,
+ nu: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ gradient: wp.array(dtype=wp.vec3),
+ hessian: wp.array(dtype=wp.mat33),
+):
+ c = wp.tid()
+
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+
+ nor = normals[c]
+ d = wp.dot(offset, nor)
+ d_hat = d / rc
+
+ stick = wp.where(d_hat < 1.0, 1.0, 0.0)
+
+ dE_d_hat = d_hat - 1.0
+ gradient[c] = dE_d_hat * stick / rc * nor
+ hessian[c] = wp.outer(nor, nor) * stick / (rc * rc)
+
+ vt = (offset - d * nor) / dt # tangential velocity
+ vt_norm = wp.length(vt)
+ vt_dir = wp.normalize(vt) # avoids dealing with 0
+
+ mu_fn = -mu * wp.min(0.0, dE_d_hat) / rc # yield force
+
+ f1_over_vt_norm = wp.where(vt_norm < 1.0, 2.0 - vt_norm, 1.0 / vt_norm)
+ gradient[c] += mu_fn * (f1_over_vt_norm + nu) * vt
+
+ # regularization such that f / H dt <= k v (penalizes friction switching dir)
+ friction_slip_reg = 0.1
+ df1_d_vtn = wp.max(
+ 2.0 * (1.0 - vt_norm),
+ friction_slip_reg / (0.5 * friction_slip_reg + vt_norm),
+ )
+
+ vt_perp = wp.cross(vt_dir, nor)
+ hessian[c] += (
+ mu_fn
+ / dt
+ * (
+ (df1_d_vtn + nu) * wp.outer(vt_dir, vt_dir)
+ + (f1_over_vt_norm + nu) * wp.outer(vt_perp, vt_perp)
+ )
+ )
+
+
+@wp.kernel
+def gather_cell_coordinates(
+ qp_cells: wp.array(dtype=int),
+ qp_coords: wp.array(dtype=wp.vec3),
+ indices: wp.array(dtype=int),
+ cells: wp.array(dtype=int),
+ coords: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+ qp = indices[i]
+
+ if qp == fem.NULL_QP_INDEX:
+ cells[i] = fem.NULL_ELEMENT_INDEX
+ else:
+ cells[i] = qp_cells[qp]
+ coords[i] = qp_coords[qp]
+
+
+@fem.integrand
+def world_position(s: fem.Sample, domain: fem.Domain, u: fem.Field):
+ return domain(s) + u(s)
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/demo_3d.py b/deps/vomp/vomp/fem/fem_examples/mfem/demo_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..c49c49d407d123576fc503309abe74996f02a85f
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/demo_3d.py
@@ -0,0 +1,184 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Domain, Sample, Field
+from warp.fem import integrand, normal
+
+from fem_examples.mfem.softbody_sim import ClassicFEM, run_softbody_sim
+from fem_examples.mfem.mfem_3d import MFEM_RS_F, MFEM_sF_S
+
+import warp.examples.fem.utils as fem_example_utils
+
+# Demo ap
+
+
+@wp.func
+def material_fraction(x: wp.vec3):
+ # arbitrary notch in the grid
+ return 1.0
+ # return wp.select(wp.length(x - wp.vec3(0.5, 1.0, 0.875)) > 0.2, 0.0, 1.0)
+
+
+@integrand
+def material_fraction_form(s: Sample, domain: Domain, phi: Field):
+ return material_fraction(domain(s)) * phi(s)
+
+
+@wp.kernel
+def mark_active(fraction: wp.array(dtype=wp.float64), active: wp.array(dtype=int)):
+ active[wp.tid()] = int(wp.nonzero(fraction[wp.tid()]))
+
+
+@integrand
+def clamped_edge(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ clamped = float(0.0)
+
+ # Single clamped edge
+ if s.qp_index < 10:
+ clamped = 1.0
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamped_right(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ pos = domain(s)
+ clamped = float(0.0)
+
+ # clamped right sides
+ clamped = wp.where(pos[0] < 1.0, 0.0, 1.0)
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamped_sides(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = normal(domain, s)
+ clamped = float(0.0)
+
+ # clamped vertical sides
+ clamped = wp.abs(nor[0])
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def boundary_displacement_form(
+ s: Sample,
+ domain: Domain,
+ v: Field,
+ displacement: float,
+):
+ """Prescribed displacement"""
+
+ # opposed to normal
+ nor = normal(domain, s)
+
+ # vertical sides only
+ clamped = wp.abs(nor[0])
+
+ return -displacement * wp.dot(nor, v(s)) * clamped
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+
+ class_parser = argparse.ArgumentParser()
+ class_parser.add_argument(
+ "--variant", "-v", choices=["mfem", "classic", "trusty"], default="mfem"
+ )
+ class_args, remaining_args = class_parser.parse_known_args()
+
+ if class_args.variant == "mfem":
+ sim_class = MFEM_RS_F
+ elif class_args.variant == "trusty":
+ sim_class = MFEM_sF_S
+ else:
+ sim_class = ClassicFEM
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument("--displacement", type=float, default=0.0)
+ parser.add_argument("--grid", action=argparse.BooleanOptionalAction)
+ parser.add_argument("--clamping", type=str, default="right")
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+ sim_class.add_parser_arguments(parser)
+ args = parser.parse_args(remaining_args)
+
+ if args.grid:
+ geo = fem.Grid3D(
+ res=wp.vec3i(args.resolution), bounds_lo=wp.vec3(0.0, 0.75, 0.75)
+ )
+ else:
+ pos, tets = fem_example_utils.gen_tetmesh(
+ res=wp.vec3i(args.resolution), bounds_lo=wp.vec3(0.0, 0.75, 0.75)
+ )
+ pos.requires_grad = True
+ geo = fem.Tetmesh(positions=pos, tet_vertex_indices=tets)
+
+ # identify cells with > 0 material fraction
+ fraction_space = fem.make_polynomial_space(geo, dtype=float, degree=0)
+ fraction_test = fem.make_test(fraction_space)
+ fraction = fem.integrate(material_fraction_form, fields={"phi": fraction_test})
+ active_cells = wp.array(dtype=int, shape=fraction.shape)
+ wp.launch(mark_active, dim=fraction.shape, inputs=[fraction, active_cells])
+
+ sim = sim_class(geo, active_cells, args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ if args.clamping == "sides":
+ boundary_projector_form = clamped_sides
+ elif args.clamping == "edge":
+ boundary_projector_form = clamped_edge
+ else:
+ boundary_projector_form = clamped_right
+
+ sim.set_boundary_condition(
+ boundary_projector_form=boundary_projector_form,
+ boundary_displacement_form=boundary_displacement_form,
+ boundary_displacement_args={
+ "displacement": args.displacement / max(1, args.n_frames)
+ },
+ )
+
+ run_softbody_sim(sim, ui=args.ui)
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/elastic_models.py b/deps/vomp/vomp/fem/fem_examples/mfem/elastic_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d1a84e56a55da64a2b7eb39879d4c7761df0ad2
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/elastic_models.py
@@ -0,0 +1,355 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import warp as wp
+import math
+
+__all__ = [
+ "hooke_energy",
+ "hooke_stress",
+ "hooke_hessian",
+ "nh_energy",
+ "nh_stress",
+ "nh_hessian_proj",
+ "nh_hessian_proj_analytic",
+ "snh_energy",
+ "snh_stress",
+ "snh_hessian_proj",
+ "snh_hessian_proj_analytic",
+]
+
+_SQRT_1_2 = wp.constant(math.sqrt(1.0 / 2.0))
+
+
+@wp.func
+def hooke_stress(S: wp.mat33, lame: wp.vec2):
+ strain = S - wp.identity(n=3, dtype=float)
+ return 2.0 * lame[1] * strain + lame[0] * wp.trace(strain) * wp.identity(
+ n=3, dtype=float
+ )
+
+
+@wp.func
+def hooke_energy(S: wp.mat33, lame: wp.vec2):
+ strain = S - wp.identity(n=3, dtype=float)
+ return 0.5 * wp.ddot(strain, hooke_stress(S, lame))
+
+
+@wp.func
+def hooke_hessian(S: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ return wp.ddot(hooke_stress(sig + wp.identity(n=3, dtype=float), lame), tau)
+
+
+# Neo-Hookean -- Eq (13) from Smith et al paper
+#
+
+
+@wp.func
+def nh_parameters_from_lame(lame: wp.vec2):
+ """Parameters such that for small strains model behaves according to Hooke's law"""
+ mu_nh = lame[1]
+ lambda_nh = lame[0] + lame[1]
+
+ return mu_nh, lambda_nh
+
+
+@wp.func
+def nh_energy(F: wp.mat33, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ E0 = lambda_nh * (1.0 - gamma) * (1.0 - gamma) + mu_nh * 3.0
+
+ return 0.5 * (lambda_nh * (J - gamma) * (J - gamma) + mu_nh * wp.ddot(F, F) - E0)
+
+
+@wp.func
+def nh_stress(F: wp.mat33, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ return mu_nh * F + lambda_nh * (J - gamma) * _dJ_dF(F)
+
+
+@wp.func
+def nh_hessian_proj(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ dJ_dF_s = _dJ_dF(F)
+
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ dpsi_dpsi = mu_nh * wp.ddot(tau, sig) + lambda_nh * wp.ddot(dJ_dF_s, tau) * wp.ddot(
+ dJ_dF_s, sig
+ )
+
+ # clamp the hessian of J so that it does not compritube eigenvalue smaller than - mu * F_scale
+ J = wp.determinant(F)
+ Ic = wp.ddot(F, F)
+ gamma = 1.0 + mu_nh / lambda_nh
+ muT = mu_nh
+ d2J_scale = _d2J_dF2_scale(J, Ic, lambda_nh * (J - gamma), 0.99 * muT)
+ # d2J_scale = lambda_nh * (J - gamma)
+ # d2J_scale = 0.0
+
+ return dpsi_dpsi + d2J_scale * _d2J_dF2(F, sig, tau)
+
+
+@wp.func
+def nh_hessian_proj_analytic(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+
+ J = wp.determinant(F)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ muT = mu_nh
+ muL = 0.0
+ lbdJ = lambda_nh * (J - gamma)
+
+ return hessian_proj_analytic(F, muT, muL, lambda_nh, lbdJ, sig, tau)
+
+
+# Stable Neo-Hookean -- complete model from Simat et al. with log(Ic + 1) term
+#
+
+
+@wp.func
+def snh_parameters_from_lame(lame: wp.vec2):
+ """Parameters such that for small strains model behaves according to Hooke's law"""
+ mu_nh = 4.0 / 3.0 * lame[1]
+ lambda_nh = lame[0] + 5.0 / 6.0 * lame[1]
+
+ return mu_nh, lambda_nh
+
+
+@wp.func
+def snh_energy(F: wp.mat33, lame: wp.vec2):
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ J = wp.determinant(F)
+ Ic = wp.ddot(F, F)
+
+ E0 = lambda_nh * (1.0 - gamma) * (1.0 - gamma) + mu_nh * (3.0 - wp.log(4.0))
+
+ return 0.5 * (
+ lambda_nh * (J - gamma) * (J - gamma) + mu_nh * (Ic - wp.log(Ic + 1.0)) - E0
+ )
+
+
+@wp.func
+def snh_stress(F: wp.mat33, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ Ic = wp.ddot(F, F)
+ F_scale = 1.0 - 1.0 / (Ic + 1.0)
+ return mu_nh * F * F_scale + lambda_nh * (J - gamma) * _dJ_dF(F)
+
+
+@wp.func
+def snh_hessian_proj(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ dJ_dF_s = _dJ_dF(F)
+
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+
+ Ic = wp.ddot(F, F)
+
+ F_scale = 1.0 - 1.0 / (Ic + 1.0)
+
+ dpsi_dpsi = (
+ mu_nh * F_scale * wp.ddot(tau, sig)
+ + 2.0 * mu_nh / ((Ic + 1.0) * (Ic + 1.0)) * wp.ddot(F, tau) * wp.ddot(F, sig)
+ + lambda_nh * wp.ddot(dJ_dF_s, tau) * wp.ddot(dJ_dF_s, sig)
+ )
+
+ # clamp the hessian of J so that it does not compritube eigenvalue smaller than - mu * F_scale
+ J = wp.determinant(F)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+ muT = mu_nh * F_scale
+ d2J_scale = _d2J_dF2_scale(J, Ic, lambda_nh * (J - gamma), 0.99 * muT)
+ # d2J_scale = lambda_nh * (J - gamma)
+ # d2J_scale = 0.0
+
+ return dpsi_dpsi + d2J_scale * _d2J_dF2(F, sig, tau)
+
+
+@wp.func
+def snh_hessian_proj_analytic(F: wp.mat33, tau: wp.mat33, sig: wp.mat33, lame: wp.vec2):
+ mu_nh, lambda_nh = snh_parameters_from_lame(lame)
+
+ Ic = wp.ddot(F, F)
+ J = wp.determinant(F)
+ gamma = 1.0 + 0.75 * mu_nh / lambda_nh
+
+ muT = mu_nh * (1.0 - 1.0 / (Ic + 1.0))
+ muL = 2.0 * mu_nh / ((Ic + 1.0) * (Ic + 1.0))
+ lbdJ = lambda_nh * (J - gamma)
+
+ return hessian_proj_analytic(F, muT, muL, lambda_nh, lbdJ, sig, tau)
+
+
+# Utilities
+
+
+@wp.func
+def hessian_proj_analytic(
+ F: wp.mat33,
+ muT: float,
+ muL: float,
+ lbd: float,
+ lbdJ: float,
+ sig: wp.mat33,
+ tau: wp.mat33,
+):
+ U = wp.mat33()
+ S = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, S, V)
+
+ # Solve eigensystem on principal stresses -- analytical is ugly
+ # (and formula (44) for eigenvectors does not seem to yield the correct result)
+ Scross = wp.vec3(S[1] * S[2], S[0] * S[2], S[0] * S[1])
+ Soff = wp.mat33(0.0, S[2], S[1], S[2], 0.0, S[0], S[1], S[0], 0.0)
+ A = (
+ muT * wp.identity(n=3, dtype=float)
+ + muL * wp.outer(S, S)
+ + lbd * wp.outer(Scross, Scross)
+ + lbdJ * Soff
+ )
+
+ Q = wp.mat33()
+ d = wp.vec3()
+ wp.eig3(A, Q, d)
+ Qt = wp.transpose(Q)
+
+ # d, Qt = fem.utils.symmetric_eigenvalues_qr(A, 1.0e-16)
+
+ # accumulate eigenvectors corresponding to positive eigenvalues
+ tauU = wp.transpose(U) * tau
+ sigU = wp.transpose(U) * sig
+ Vt = wp.transpose(V)
+
+ res = float(0.0)
+ clamp = 0.0
+
+ for k in range(3):
+ Pk = wp.diag(Qt[k]) * Vt
+ res += wp.max(clamp, d[k]) * wp.ddot(tauU, Pk) * wp.ddot(sigU, Pk)
+
+ Pk = _flip_rot_eivec(k, 1.0, Vt)
+ res += wp.max(clamp, muT + lbdJ * S[k]) * wp.ddot(tauU, Pk) * wp.ddot(sigU, Pk)
+
+ Pk = _flip_rot_eivec(k, -1.0, Vt)
+ res += wp.max(clamp, muT - lbdJ * S[k]) * wp.ddot(tauU, Pk) * wp.ddot(sigU, Pk)
+
+ return res
+
+
+@wp.func
+def _flip_rot_eivec(k: int, sign: float, mat: wp.mat33):
+ E = wp.mat33(0.0)
+ k2 = (k + 2) % 3
+ k1 = (k + 1) % 3
+ E[k1] = _SQRT_1_2 * mat[k2]
+ E[k2] = -sign * _SQRT_1_2 * mat[k1]
+ return E
+
+
+@wp.func
+def _dJ_dF(F: wp.mat33):
+ Ft = wp.transpose(F)
+ return wp.mat33(
+ wp.cross(Ft[1], Ft[2]), wp.cross(Ft[2], Ft[0]), wp.cross(Ft[0], Ft[1])
+ )
+
+
+@wp.func
+def _d2J_dF2(F: wp.mat33, sig: wp.mat33, tau: wp.mat33):
+ Ft = wp.transpose(F)
+ sigt = wp.transpose(sig)
+ return wp.ddot(
+ tau,
+ wp.mat33(
+ wp.cross(Ft[1], sigt[2]) + wp.cross(sigt[1], Ft[2]),
+ wp.cross(Ft[2], sigt[0]) + wp.cross(sigt[2], Ft[0]),
+ wp.cross(Ft[0], sigt[1]) + wp.cross(sigt[0], Ft[1]),
+ ),
+ )
+
+
+@wp.func
+def _d2J_dF2_scale(J: float, Ic: float, J_scale: float, Id_scale: float):
+ # compute a scaling for d2J such that Id_scale * Id + J_scale * d2J
+ # has no negative eigenvalues
+
+ # Min/max eigenvalues for d2J are estimated according to
+ # sec 4.5 of "Stable Neo-Hookean Flesh Simulation" (Smith et al. 2018)
+
+ d2J_ev = _depressed_cubic_roots(-Ic, -2.0 * J)
+ sig_max = wp.sqrt(Ic)
+
+ ev_min = wp.min(wp.min(d2J_ev), -sig_max)
+ ev_max = wp.max(wp.max(d2J_ev), sig_max)
+ return wp.clamp(J_scale, -Id_scale / ev_max, -Id_scale / ev_min)
+
+
+@wp.func
+def _depressed_cubic_roots(p: float, q: float):
+ alpha = wp.sqrt(-p / 3.0)
+ beta = wp.acos(1.5 * q / (p * alpha)) / 3.0
+ return (
+ 2.0
+ * alpha
+ * wp.vec3(
+ wp.cos(beta),
+ wp.cos(beta - 2.0 / 3.0 * wp.pi),
+ wp.cos(beta - 4.0 / 3.0 * wp.pi),
+ )
+ )
+
+
+@wp.func
+def symmetric_strain(F: wp.mat33):
+ U = wp.mat33()
+ sig = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, sig, V)
+
+ S = V * wp.diag(sig) * wp.transpose(V)
+
+ return S
+
+
+@wp.func
+def symmetric_strain_delta(F: wp.mat33, dF: wp.mat33):
+ # see supplementary of `WRAPD: Weighted Rotation-aware ADMM`, Brown and Narain 21
+
+ U = wp.mat33()
+ sig = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, sig, V)
+
+ Ut = wp.transpose(U)
+ Vt = wp.transpose(V)
+
+ dF_loc = Ut * dF * V
+ SigdF_loc = wp.diag(sig) * dF_loc
+
+ sig_op = wp.matrix_from_cols(wp.vec3(sig[0]), wp.vec3(sig[1]), wp.vec3(sig[2]))
+ dSig = wp.cw_div(SigdF_loc + wp.transpose(SigdF_loc), sig_op + wp.transpose(sig_op))
+ dS = V * dSig * Vt
+
+ return dS
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/linalg.py b/deps/vomp/vomp/fem/fem_examples/mfem/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..71586701c7498643f2244b1ef7ba91ee1dc2430e
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/linalg.py
@@ -0,0 +1,470 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Any, Tuple
+
+import numpy as np
+import warp as wp
+import warp.sparse as sp
+from warp.fem.utils import inverse_qr
+from warp.fem.utils import array_axpy
+
+from warp.examples.fem.utils import bsr_cg
+
+wp.set_module_options({"enable_backward": False})
+wp.set_module_options({"fast_math": True})
+
+
+def diff_bsr_mv(
+ A: sp.BsrMatrix,
+ x: wp.array,
+ y: wp.array,
+ alpha: float = 1.0,
+ beta: float = 0.0,
+ transpose: bool = False,
+ self_adjoint: bool = False,
+):
+ """Performs y = alpha*A*x + beta*y and records the adjoint on the tape"""
+
+ from warp.context import runtime
+
+ tape = runtime.tape
+ if tape is not None and (x.requires_grad or y.requires_grad):
+
+ def backward():
+ # adj_x += adj_y * alpha
+ # adj_y = adj_y * beta
+
+ sp.bsr_mv(
+ A=A,
+ x=y.grad,
+ y=x.grad,
+ alpha=alpha,
+ beta=1.0,
+ transpose=(not transpose) and (not self_adjoint),
+ )
+ if beta != 1.0:
+ array_axpy(x=y.grad, y=y.grad, alpha=0.0, beta=beta)
+
+ runtime.tape.record_func(backward, arrays=[x, y])
+
+ runtime.tape = None
+ # in case array_axpy eventually records its own stuff
+ sp.bsr_mv(A, x, y, alpha, beta, transpose)
+ runtime.tape = tape
+
+
+# MFEM SYSTEM
+# for F = RS variant
+#
+# [ A -B' ]
+# [ H Cd' ]
+# [ W Cr' ]
+# [ -B Cs Cr 0 ]
+#
+
+
+class MFEMSystem:
+ """Builds a linear operator corresponding to the saddle-point linear system [A B^T; B 0]"""
+
+ def __init__(
+ self,
+ A: sp.BsrMatrix,
+ H: sp.BsrMatrix,
+ W: sp.BsrMatrix,
+ B: sp.BsrMatrix,
+ Cs: sp.BsrMatrix,
+ Cr: sp.BsrMatrix,
+ Bt: sp.BsrMatrix = None,
+ ):
+ self._A = A
+ self._H = H
+ self._W = W
+ self._B = B
+ self._Bt = Bt
+ self._Cs = Cs
+ self._Cr = Cr
+
+ def cast(self, scalar_type):
+ if wp.types.types_equal(scalar_type, self._A.scalar_type):
+ return self
+
+ return MFEMSystem(
+ sp.bsr_copy(self._A, scalar_type=scalar_type),
+ sp.bsr_copy(self._H, scalar_type=scalar_type),
+ (
+ sp.bsr_copy(self._W, scalar_type=scalar_type)
+ if self._W is not None
+ else None
+ ),
+ sp.bsr_copy(self._B, scalar_type=scalar_type),
+ sp.bsr_copy(self._Cs, scalar_type=scalar_type),
+ (
+ sp.bsr_copy(self._Cr, scalar_type=scalar_type)
+ if self._W is not None
+ else None
+ ),
+ )
+
+ def solve_schur(
+ lhs,
+ rhs: Tuple,
+ tol=1.0e-8,
+ max_iters=1000,
+ work_arrays=None,
+ reuse_topology=False,
+ ):
+ rhs_type = wp.types.type_scalar_type(rhs[0].dtype)
+ lhs_type = lhs._A.scalar_type
+
+ if not wp.types.types_equal(lhs_type, rhs_type):
+ rhs_cast = tuple(
+ wp.empty(
+ shape=v.shape,
+ dtype=wp.vec(length=wp.types.type_length(v.dtype), dtype=lhs_type),
+ )
+ for v in rhs
+ )
+ for v, v_cast in zip(rhs, rhs_cast):
+ wp.utils.array_cast(in_array=v, out_array=v_cast)
+
+ res = lhs.solve_schur(
+ rhs_cast,
+ tol=tol,
+ max_iters=max_iters,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+
+ res_cast = tuple(
+ wp.empty(
+ shape=v.shape,
+ dtype=wp.vec(length=wp.types.type_length(v.dtype), dtype=rhs_type),
+ )
+ for v in res
+ )
+ for v, v_cast in zip(res, res_cast):
+ wp.utils.array_cast(in_array=v, out_array=v_cast)
+
+ return res_cast
+
+ if lhs._Cr is None:
+ return lhs.solve_schur_no_R(
+ rhs,
+ tol=tol,
+ max_iters=max_iters,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+
+ u_rhs, f, w_lambda, c_k = rhs
+
+ A = lhs._A
+ H = lhs._H
+ W_skew = lhs._W
+ B = lhs._B
+ CSk = lhs._Cs
+ CRk = lhs._Cr
+
+ u_matrix = sp.bsr_copy(A)
+
+ H_inv = wp.empty_like(H.values)
+ W_skew_inv = wp.empty_like(W_skew.values)
+ CHiCt_inv = wp.empty(
+ shape=H.nrow, dtype=wp.mat(shape=(9, 9), dtype=H.scalar_type)
+ )
+ lambda_rhs = wp.clone(c_k, requires_grad=False)
+
+ wp.launch(
+ invert_blocks,
+ dim=W_skew.nnz,
+ inputs=[W_skew.values, W_skew_inv],
+ device=W_skew.device,
+ )
+ wp.launch(
+ invert_blocks,
+ dim=H.nnz,
+ inputs=[H.values, H_inv],
+ device=H.device,
+ )
+
+ wp.launch(
+ kernel=compute_first_schur,
+ dim=CHiCt_inv.shape,
+ inputs=[
+ CHiCt_inv,
+ CSk.values,
+ CRk.values,
+ H_inv,
+ W_skew_inv,
+ lambda_rhs,
+ f,
+ w_lambda,
+ ],
+ )
+
+ BtCHiCt_inv = sp.bsr_transposed(B) if lhs._Bt is None else sp.bsr_copy(lhs._Bt)
+ wp.launch(
+ bsr_mul_diag,
+ dim=BtCHiCt_inv.nnz,
+ inputs=[BtCHiCt_inv.values, BtCHiCt_inv.columns, CHiCt_inv],
+ )
+
+ sp.bsr_mm(
+ x=BtCHiCt_inv,
+ y=B,
+ z=u_matrix,
+ alpha=1.0,
+ beta=1.0,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+ sp.bsr_mv(A=BtCHiCt_inv, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ delta_du = wp.zeros_like(u_rhs)
+ err, niter = bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=tol, max_iters=max_iters, quiet=True
+ )
+
+ if np.isnan(err):
+ raise RuntimeError(f"Solver fail, rhs= {np.linalg.norm(u_rhs.numpy())}")
+
+ # other variable updates
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ sp.bsr_mv(A=B, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+ dLambda = wp.empty_like(lambda_rhs)
+ dS = wp.empty_like(f)
+ dR = wp.empty_like(w_lambda)
+
+ wp.launch(
+ kernel=compute_dLambdadRdS,
+ dim=H_inv.shape[0],
+ inputs=[
+ CSk.values,
+ CRk.values,
+ CHiCt_inv,
+ H_inv,
+ W_skew_inv,
+ lambda_rhs,
+ f,
+ w_lambda,
+ dLambda,
+ dS,
+ dR,
+ ],
+ )
+
+ return delta_du, dS, dR, dLambda
+
+ def solve_schur_no_R(
+ lhs,
+ rhs: Tuple,
+ tol=1.0e-8,
+ max_iters=1000,
+ work_arrays=None,
+ reuse_topology=False,
+ ):
+ u_rhs, f, w_lambda, c_k = rhs
+
+ A = lhs._A
+ H = lhs._H
+ B = lhs._B
+ CSk = lhs._Cs
+
+ u_matrix = sp.bsr_copy(A)
+
+ Cs_inv = sp.bsr_copy(CSk)
+ wp.launch(
+ invert_blocks,
+ dim=Cs_inv.nnz,
+ inputs=[CSk.values, Cs_inv.values],
+ device=H.device,
+ )
+
+ CHiCt_inv = wp.empty(
+ shape=H.nrow, dtype=wp.mat(shape=(6, 6), dtype=H.scalar_type)
+ )
+
+ wp.launch(
+ kernel=compute_first_schur_no_R,
+ dim=CHiCt_inv.shape,
+ inputs=[
+ CHiCt_inv,
+ Cs_inv.values,
+ H.values,
+ ],
+ )
+
+ ci_f = Cs_inv @ f
+ Bt = sp.bsr_transposed(B) if lhs._Bt is None else sp.bsr_copy(lhs._Bt)
+ sp.bsr_mv(A=Bt, x=ci_f, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ BtCHiCt_inv = Bt
+ wp.launch(
+ bsr_mul_diag,
+ dim=BtCHiCt_inv.nnz,
+ inputs=[BtCHiCt_inv.values, BtCHiCt_inv.columns, CHiCt_inv],
+ )
+
+ sp.bsr_mm(
+ x=BtCHiCt_inv,
+ y=B,
+ z=u_matrix,
+ alpha=1.0,
+ beta=1.0,
+ work_arrays=work_arrays,
+ reuse_topology=reuse_topology,
+ )
+ sp.bsr_mv(A=BtCHiCt_inv, x=c_k, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ delta_du = wp.zeros_like(u_rhs)
+ err, niter = bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=tol, max_iters=max_iters, quiet=True
+ )
+
+ if np.isnan(err):
+ raise RuntimeError(f"Solver fail, rhs= {np.linalg.norm(u_rhs.numpy())}")
+
+ # other variable updates
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ lambda_rhs = wp.clone(c_k, requires_grad=False)
+ sp.bsr_mv(A=B, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ dLambda = wp.empty_like(lambda_rhs)
+ dS = wp.empty_like(f)
+ dR = wp.empty_like(w_lambda)
+
+ wp.launch(
+ kernel=compute_dLambdadS,
+ dim=Cs_inv.values.shape[0],
+ inputs=[
+ Cs_inv.values,
+ CHiCt_inv,
+ lambda_rhs,
+ ci_f,
+ dLambda,
+ dS,
+ ],
+ )
+
+ return delta_du, dS, dR, dLambda
+
+
+@wp.func
+def invert_schur_block(M: Any):
+ eps = type(M[0])(M.dtype(1.0e-16))
+ return inverse_qr(M + wp.diag(eps))
+
+
+@wp.kernel
+def compute_first_schur(
+ CHiC_inv: wp.array(dtype=Any),
+ Cs: wp.array(dtype=Any),
+ Cr: wp.array(dtype=Any),
+ H_inv: wp.array(dtype=Any),
+ W_inv: wp.array(dtype=Any),
+ lambda_rhs: wp.array(dtype=Any),
+ f: wp.array(dtype=Any),
+ w_lambda: wp.array(dtype=Any),
+):
+ i = wp.tid()
+
+ cr = Cr[i]
+ cs = Cs[i]
+
+ csHi = cs * H_inv[i]
+ crWi = cr * W_inv[i]
+
+ lambda_rhs[i] += csHi * f[i] + crWi * w_lambda[i]
+
+ CHiC = csHi * wp.transpose(cs) + crWi * wp.transpose(cr)
+ CHiC_inv[i] = invert_schur_block(CHiC)
+
+
+@wp.kernel
+def compute_dLambdadRdS(
+ Cs: wp.array(dtype=Any),
+ Cr: wp.array(dtype=Any),
+ C_inv: wp.array(dtype=Any),
+ H_inv: wp.array(dtype=Any),
+ W_inv: wp.array(dtype=Any),
+ lambda_rhs: wp.array(dtype=Any),
+ f: wp.array(dtype=Any),
+ w_lambda: wp.array(dtype=Any),
+ dLambda: wp.array(dtype=Any),
+ dS: wp.array(dtype=Any),
+ dR: wp.array(dtype=Any),
+):
+ i = wp.tid()
+ dL = -C_inv[i] * lambda_rhs[i]
+ dLambda[i] = dL
+ dS[i] = -H_inv[i] * (f[i] + wp.transpose(Cs[i]) * dL)
+ dR[i] = -W_inv[i] * (w_lambda[i] + wp.transpose(Cr[i]) * dL)
+
+
+@wp.kernel
+def compute_first_schur_no_R(
+ CHiC_inv: wp.array(dtype=Any),
+ Csi: wp.array(dtype=Any),
+ H: wp.array(dtype=Any),
+):
+ i = wp.tid()
+
+ CHiC_inv[i] = Csi[i] * H[i] * Csi[i]
+
+
+@wp.kernel
+def compute_dLambdadS(
+ Csi: wp.array(dtype=Any),
+ CHiC_inv: wp.array(dtype=Any),
+ lambda_rhs: wp.array(dtype=Any),
+ ci_f: wp.array(dtype=Any),
+ dLambda: wp.array(dtype=Any),
+ dS: wp.array(dtype=Any),
+):
+ i = wp.tid()
+
+ dLambda[i] = -CHiC_inv[i] * lambda_rhs[i] - ci_f[i]
+ dS[i] = Csi[i] * lambda_rhs[i]
+
+
+@wp.kernel
+def invert_blocks(A: wp.array(dtype=Any), A_inv: wp.array(dtype=Any)):
+ i = wp.tid()
+ A_inv[i] = inverse_qr(A[i])
+
+
+@wp.kernel
+def invert_schur_blocks(values: wp.array(dtype=Any)):
+ i = wp.tid()
+
+ values[i] = invert_schur_block(values[i])
+
+
+@wp.kernel
+def bsr_mul_diag(
+ Bt_values: wp.array(dtype=Any),
+ Bt_columns: wp.array(dtype=int),
+ C_values: wp.array(dtype=Any),
+):
+ i = wp.tid()
+ col = Bt_columns[i]
+ Bt_values[i] *= C_values[col]
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/mfem_2d.py b/deps/vomp/vomp/fem/fem_examples/mfem/mfem_2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..afa342ff9f48fba9a019864102c1ef4fcd065a3c
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/mfem_2d.py
@@ -0,0 +1,1664 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+
+import warp as wp
+
+import numpy as np
+
+from warp.fem import Domain, Sample, Field
+from warp.fem import normal, integrand, grad
+import warp.fem as fem
+
+from warp.fem.utils import array_axpy
+from warp.sparse import bsr_transposed, bsr_mm, bsr_axpy, bsr_mv, bsr_copy
+
+import warp.examples.fem.utils as fem_example_utils
+
+import matplotlib.pyplot as plt
+import matplotlib.animation as animation
+
+import math
+
+_SQRT_2 = wp.constant(math.sqrt(2.0))
+_SQRT_1_2 = wp.constant(math.sqrt(1.0 / 2.0))
+
+
+class FullTensorMapper(fem.DofMapper):
+ """Orthonormal isomorphism from R^{n (n+1)} to nxn symmetric tensors,
+ using usual L2 norm for vectors and half Frobenius norm, (tau : tau)/2 for tensors.
+ """
+
+ def __init__(self, dtype: type):
+ self.value_dtype = dtype
+ self.DOF_SIZE = wp.constant(4)
+ self.dof_dtype = wp.vec4
+
+ def __str__(self):
+ return f"_{self.DOF_SIZE}"
+
+ @wp.func
+ def dof_to_value(dof: wp.vec4):
+ a = _SQRT_2 * dof[0]
+ b = _SQRT_2 * dof[1]
+ c = dof[2]
+ d = dof[3]
+ return wp.mat22(a, c - d, c + d, b)
+
+ @wp.func
+ def value_to_dof(val: wp.mat22):
+ a = _SQRT_1_2 * val[0, 0]
+ b = _SQRT_1_2 * val[1, 1]
+ c = 0.5 * (val[0, 1] + val[1, 0])
+ d = 0.5 * (val[1, 0] - val[0, 1])
+ return wp.vec4(a, b, c, d)
+
+
+@wp.func
+def hooke_stress(strain: wp.mat22, lame: wp.vec2):
+ return 2.0 * lame[1] * strain + lame[0] * wp.trace(strain) * wp.identity(
+ n=2, dtype=float
+ )
+
+
+@integrand
+def linear_elasticity_hessian_form(
+ s: Sample, S: Field, tau: Field, sig: Field, lame: wp.vec2
+):
+ return wp.ddot(hooke_stress(sig(s), lame), tau(s))
+
+
+@integrand
+def linear_elasticity_gradient_form(s: Sample, tau: Field, S: Field, lame: wp.vec2):
+ return wp.ddot(hooke_stress(S(s) - wp.identity(n=2, dtype=float), lame), tau(s))
+
+
+@integrand
+def linear_elasticity_energy(s: Sample, S: Field, lame: wp.vec2):
+ strain = S(s) - wp.identity(n=2, dtype=float)
+ return 0.5 * wp.ddot(strain, hooke_stress(strain, lame))
+
+
+@wp.func
+def nh_parameters_from_lame(lame: wp.vec2):
+ """Parameters such that for small strains model behaves according to Hooke's law"""
+ mu_nh = lame[1]
+ lambda_nh = lame[0] + lame[1]
+
+ return mu_nh, lambda_nh
+
+
+@wp.func
+def nh_energy(F: wp.mat22, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ return 0.5 * lambda_nh * (J - gamma) * (J - gamma) + 0.5 * mu_nh * wp.ddot(F, F)
+
+
+@wp.func
+def nh_stress(F: wp.mat22, lame: wp.vec2):
+ J = wp.determinant(F)
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+ gamma = 1.0 + mu_nh / lambda_nh
+
+ dJ_dF = wp.mat22(F[1, 1], -F[1, 0], -F[0, 1], F[0, 0])
+ return mu_nh * F + (lambda_nh * (J - gamma)) * dJ_dF
+
+
+@integrand
+def nh_elasticity_hessian_form(
+ s: Sample, S: Field, tau: Field, sig: Field, lame: wp.vec2
+):
+ tau_s = tau(s)
+ sig_s = sig(s)
+
+ F_s = S(s)
+ dJ_dF = wp.mat22(F_s[1, 1], -F_s[1, 0], -F_s[0, 1], F_s[0, 0])
+
+ mu_nh, lambda_nh = nh_parameters_from_lame(lame)
+
+ dpsi_dpsi = mu_nh * wp.ddot(tau_s, sig_s) + lambda_nh * wp.ddot(
+ dJ_dF, tau_s
+ ) * wp.ddot(dJ_dF, sig_s)
+
+ # SPD projection of (J - gamma) d2J_dS2
+ gamma = 1.0 + mu_nh / lambda_nh
+ J = wp.determinant(F_s)
+
+ d2J_dF_pos = wp.mat22(
+ sig_s[1, 1] + sig_s[0, 0],
+ sig_s[0, 1] - sig_s[1, 0],
+ sig_s[1, 0] - sig_s[0, 1],
+ sig_s[0, 0] + sig_s[1, 1],
+ )
+ d2J_dF_neg = wp.mat22(
+ sig_s[1, 1] - sig_s[0, 0],
+ -sig_s[0, 1] - sig_s[1, 0],
+ -sig_s[1, 0] - sig_s[0, 1],
+ sig_s[0, 0] - sig_s[1, 1],
+ )
+
+ d2J_dF = wp.min(0.5 * lambda_nh * (J - gamma), mu_nh) * d2J_dF_neg
+ d2J_dF += wp.max(0.5 * lambda_nh * (J - gamma), -mu_nh) * d2J_dF_pos
+
+ return dpsi_dpsi + wp.ddot(d2J_dF, tau_s)
+
+
+@integrand
+def nh_elasticity_gradient_form(s: Sample, tau: Field, S: Field, lame: wp.vec2):
+ return wp.ddot(tau(s), nh_stress(S(s), lame))
+
+
+@integrand
+def nh_elasticity_energy(s: Sample, S: Field, lame: wp.vec2):
+ return nh_energy(S(s), lame)
+
+
+@integrand
+def tensor_mass_form(s: Sample, sig: Field, tau: Field):
+ """
+ Mass form over tensor space
+ sig : tau
+ """
+ return wp.ddot(sig(s), tau(s))
+
+
+@integrand
+def boundary_projector_form(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = normal(domain, s)
+ clamped = float(0.0)
+
+ # Single clamped point
+ if s.qp_index == 0:
+ clamped = 1.0
+
+ # clamped vertical sides
+ # clamped = wp.abs(nor[0])
+
+ # clamped right sides
+ # clamped = wp.max(0.0, nor[0])
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def boundary_displacement_form(
+ s: Sample,
+ domain: Domain,
+ v: Field,
+ displacement: float,
+):
+ """Prescribed displacement"""
+
+ # opposed to normal
+ nor = normal(domain, s)
+ return -displacement * wp.dot(nor, v(s))
+
+
+@integrand
+def inertia_form(s: Sample, u: Field, v: Field, rho: float, dt: float):
+ """"""
+
+ u_rhs = rho * u(s) / (dt * dt)
+ return wp.dot(u_rhs, v(s))
+
+
+@integrand
+def dg_penalty_form(s: Sample, domain: Domain, u: Field, v: Field, k: float):
+ ju = fem.jump(u, s)
+ jv = fem.jump(v, s)
+
+ return wp.dot(ju, jv) * k / 10.0 * fem.measure_ratio(domain, s)
+
+
+@integrand
+def displacement_rhs_form(
+ s: Sample, u_cur: Field, u: Field, v: Field, rho: float, gravity: wp.vec2, dt: float
+):
+ """ + """
+
+ return (
+ inertia_form(s, u, v, rho, dt)
+ - inertia_form(s, u_cur, v, rho, dt)
+ + rho * wp.dot(gravity, v(s))
+ )
+
+
+@integrand
+def kinetic_potential_energy(
+ s: Sample, u: Field, v: Field, rho: float, dt: float, gravity: wp.vec2
+):
+ du = u(s)
+ dv = v(s)
+ return rho * (0.5 * wp.dot(du - dv, du - dv) / (dt * dt) - wp.dot(du, gravity))
+
+
+@wp.func
+def rotation_matrix(angle: float):
+ # return wp.identity(n=2, dtype=wp.float32)
+
+ c = wp.cos(angle)
+ s = wp.sin(angle)
+ return wp.mat22(c, -s, s, c)
+
+
+@wp.kernel
+def apply_rotation_delta(
+ R: wp.array(dtype=float), dR: wp.array(dtype=float), alpha: float
+):
+ i = wp.tid()
+ R[i] = R[i] + dR[i] * alpha
+
+
+class MFEM:
+ def __init__(self, args):
+ self.args = args
+
+ if args.grid:
+ self.geo = fem.Grid2D(
+ res=wp.vec2i(args.resolution), bounds_lo=wp.vec2(0.0, 0.75)
+ )
+ else:
+ positions, tri_vidx = fem_example_utils.gen_trimesh(
+ res=wp.vec2i(args.resolution), bounds_lo=wp.vec2(0.0, 0.75)
+ )
+ self.geo = fem.Trimesh2D(tri_vertex_indices=tri_vidx, positions=positions)
+
+ print("Cell area", 0.25 / self.geo.cell_count())
+
+ # Strain-stress matrix
+ young = args.young_modulus
+ poisson = args.poisson_ratio
+ self.lame = wp.vec2(
+ young / (1.0 + poisson) * np.array([poisson / (1.0 - poisson), 0.5])
+ )
+
+ self.dt = args.dt
+ self.gravity = wp.vec2(0.0, -args.gravity)
+ self.rot_stiff = 1.0 / args.rot_compliance
+
+ if args.grid:
+ self.strain_degree = args.degree
+ self.rot_degree = args.degree
+ self.strain_basis = fem.ElementBasis.LAGRANGE
+ else:
+ self.strain_degree = args.degree - 1
+ self.rot_degree = args.degree - 1
+ self.strain_basis = fem.ElementBasis.NONCONFORMING_POLYNOMIAL
+
+ self.strain_poly = fem.Polynomial.GAUSS_LEGENDRE
+
+ if args.neo_hookean:
+ self.elastic_energy_form = nh_elasticity_energy
+ self.elastic_gradient_form = nh_elasticity_gradient_form
+ self.elastic_hessian_form = nh_elasticity_hessian_form
+ else:
+ self.elastic_energy_form = linear_elasticity_energy
+ self.elastic_gradient_form = linear_elasticity_gradient_form
+ self.elastic_hessian_form = linear_elasticity_hessian_form
+
+ def init_vel_space(self):
+ # Function spaces -- Q_k for displacement, Q_{k-1}d for stress
+ u_space = fem.make_polynomial_space(
+ self.geo,
+ degree=args.degree,
+ dtype=wp.vec2,
+ discontinuous=False,
+ element_basis=(
+ fem.ElementBasis.SERENDIPITY if self.args.serendipity else None
+ ),
+ )
+
+ # Defines some fields over our function spaces
+ self.u_field = u_space.make_field() # displacement
+ self.du_field = u_space.make_field() # displacement delta
+ self.du_prev = u_space.make_field() # displacement delta
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ domain = fem.Cells(self.geo)
+ self.u_trial = fem.make_trial(space=u_space, domain=domain)
+ self.u_test = fem.make_test(space=u_space, domain=domain)
+
+ sides = fem.Sides(self.geo)
+ self.u_side_trial = fem.make_trial(space=u_space, domain=sides)
+ self.u_side_test = fem.make_test(space=u_space, domain=sides)
+
+ def init_strain_spaces(self):
+ args = self.args
+
+ # Store stress degrees of freedom as symmetric tensors (3 dof) rather than full 2x2 matrices
+ sym_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ dof_mapper=fem.SymmetricTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ # Function spaces for piecewise-constant per-element rotations and rotation vectors
+ rot_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.rot_degree,
+ discontinuous=True,
+ dtype=float,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+ skew_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.rot_degree,
+ dof_mapper=fem.SkewSymmetricTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ # Defines some fields over our function spaces
+
+ self.S = sym_space.make_field() # Rotated symmetric train
+ self.S.dof_values.fill_(
+ sym_space.dof_mapper.value_to_dof(wp.mat22(1.0, 0.0, 0.0, 1.0))
+ ) # initialize with identity
+
+ self.R = rot_space.make_field() # Rotation
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ domain = fem.Cells(self.geo)
+ self.sym_test = fem.make_test(space=sym_space, domain=domain)
+ self.sym_trial = fem.make_trial(
+ space=sym_space,
+ space_partition=self.sym_test.space_partition,
+ domain=domain,
+ )
+
+ if skew_space.degree == sym_space.degree:
+ self.skew_test = fem.make_test(
+ space=skew_space,
+ space_partition=self.sym_test.space_partition,
+ domain=domain,
+ )
+ self.skew_trial = fem.make_trial(
+ space=skew_space,
+ space_partition=self.sym_test.space_partition,
+ domain=domain,
+ )
+ else:
+ self.skew_test = fem.make_test(space=skew_space, domain=domain)
+ self.skew_trial = fem.make_trial(space=skew_space, domain=domain)
+
+ self.quadrature = fem.RegularQuadrature(domain, order=2 * args.degree)
+
+ def init_boundary_conditions(self):
+ u_space = self.u_field.space
+
+ # Displacement boundary conditions
+ # For simplicity, assume constant per-frame displacement
+ boundary = fem.BoundarySides(self.geo)
+ u_bd_test = fem.make_test(space=u_space, domain=boundary)
+ u_bd_trial = fem.make_trial(space=u_space, domain=boundary)
+ self.v_bd_rhs = fem.integrate(
+ boundary_displacement_form,
+ fields={"v": u_bd_test},
+ values={"displacement": args.displacement / args.n_frames},
+ nodal=True,
+ output_dtype=wp.vec2f,
+ )
+ self.v_bd_matrix = fem.integrate(
+ boundary_projector_form,
+ fields={"u": u_bd_trial, "v": u_bd_test},
+ nodal=True,
+ output_dtype=float,
+ )
+ fem.normalize_dirichlet_projector(self.v_bd_matrix, self.v_bd_rhs)
+
+ def init_constant_forms(self):
+ self.A = fem.integrate(
+ inertia_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt},
+ output_dtype=float,
+ ) + fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_side_trial, "v": self.u_side_test},
+ values={"k": self.lame[0]},
+ output_dtype=float,
+ )
+
+ self.Ci = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.constraint_test, "sig": self.constraint_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ fem_example_utils.invert_diagonal_bsr_matrix(self.Ci)
+
+ def run_frame(self):
+ (self.du_field, self.du_prev) = (self.du_prev, self.du_field)
+
+ self.compute_initial_guess()
+
+ tol = 1.0e-8
+
+ for k in range(self.args.n_newton):
+ E_ref = self.evaluate_energy()
+
+ ddu, dR, dS = self.newton_iter(k)
+ self.apply_newton_deltas(ddu, dR, dS)
+
+ step_size = wp.utils.array_inner(ddu, ddu) / (1 + ddu.shape[0])
+
+ # Line search
+ alpha = 1.0
+ for j in range(self.args.n_backtrack):
+ E_cur = self.evaluate_energy()
+ if E_cur < E_ref:
+ break
+
+ alpha = 0.5 * alpha
+ self.apply_newton_deltas(ddu, dR, dS, alpha=-alpha)
+
+ print(f"Newton iter {k}: step size {step_size}, alpha={alpha}")
+
+ if step_size < tol:
+ break
+
+ def assemble_constraint_free_system(self, with_external_forces=True):
+ gravity = self.gravity if with_external_forces else wp.vec2(0.0)
+
+ l = fem.integrate(
+ displacement_rhs_form,
+ fields={"u_cur": self.du_field, "u": self.du_prev, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt, "gravity": gravity},
+ output_dtype=wp.vec2,
+ )
+
+ pen_rhs = fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_side_test},
+ values={"k": self.lame[0]},
+ output_dtype=wp.vec2,
+ )
+ fem.utils.array_axpy(x=pen_rhs, y=l, alpha=-1, beta=1)
+
+ return self.A, l
+
+ def apply_newton_deltas(self, delta_du, dR, dS, alpha=1.0):
+ # Add to total displacement
+ array_axpy(x=delta_du, y=self.u_field.dof_values, alpha=alpha)
+ array_axpy(x=delta_du, y=self.du_field.dof_values, alpha=alpha)
+
+ array_axpy(x=dS, y=self.S.dof_values, alpha=alpha)
+
+ # Apply rotation delta
+ wp.launch(
+ kernel=apply_rotation_delta,
+ dim=self.R.space.node_count(),
+ inputs=[self.R.dof_values, dR, alpha],
+ )
+
+ def evaluate_energy(self, include_constraint_residual=True):
+ E_e = fem.integrate(
+ self.elastic_energy_form,
+ quadrature=fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * self.args.degree
+ ),
+ fields={"S": self.S},
+ values={"lame": self.lame},
+ )
+ E_u = fem.integrate(
+ kinetic_potential_energy,
+ quadrature=self.quadrature,
+ fields={"u": self.du_field, "v": self.du_prev},
+ values={"rho": self.args.density, "dt": self.dt, "gravity": self.gravity},
+ )
+ E_pen = fem.integrate(
+ dg_penalty_form,
+ domain=fem.Sides(self.geo),
+ fields={"u": self.u_field.trace(), "v": self.u_field.trace()},
+ values={"k": self.lame[0]},
+ )
+
+ E_tot = E_u + E_e + E_pen
+
+ if include_constraint_residual:
+ ck_field = self.evaluate_ck()
+
+ ck_field.dof_values = self.Ci @ ck_field.dof_values
+
+ c_r = (
+ fem.integrate(
+ tensor_mass_form,
+ quadrature=self.quadrature,
+ fields={"sig": ck_field, "tau": ck_field},
+ )
+ * self.lame[0]
+ )
+ E_tot += c_r
+
+ return E_tot
+
+ def compute_initial_guess(self):
+ # Self-advect
+ A, l = self.assemble_constraint_free_system(with_external_forces=False)
+ u_rhs = l
+ u_matrix = bsr_copy(self.A)
+
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, self.v_bd_rhs, normalize_projector=False
+ )
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=self.du_field.dof_values, quiet=True
+ )
+ array_axpy(x=self.du_field.dof_values, y=self.u_field.dof_values)
+
+
+class MFEM_S_RF(MFEM):
+ """S = RF variant"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.W = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.sym_test, "sig": self.sym_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_inv = bsr_copy(self.W)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_inv)
+
+ self.W_skew = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.skew_test, "sig": self.skew_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_skew_inv = bsr_copy(self.W_skew)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_skew_inv)
+
+ def newton_iter(self, k: int):
+ # argmin_u,S,R argmax lambda V(u, u) + psi(S) + c(dR) + w(S, lambda) - b(R, u, lambda)
+ #
+ # with lambda = lambda_sym + lambda_skew
+ #
+ # a(dU, v) - b(R, lambda, v) = l(v) - a(u, v forall v in R^3
+ # w(dS, tau) - b(R, dU, tau) = b(R, u, tau) - w(S, tau) forall tau in sym(3x3)
+ # -b(R, dU, tau) - b(dR, u, tau) = b(R, u, tau) forall tau in skew(3x3)
+ # h(S; dS, tau) + w(tau, lambda) = -f(S; tau) forall tau in sym(3x3)
+ # w(dR, tau) - b(tau, u, lambda) = 0 forall tau in skew(3x3)
+ #
+ # a(u, v) = int( rho/dt )
+ # l(v) = int( <(rho/dt v^0 + rho g), v> )
+ #
+ # b(R, u, tau) = int ( R (I + grad u) : tau)
+ # w(sig, tau) = int ( sig : tau)
+ #
+ # H(S; sig, tau) = int ( sig : (d2 psi/dS2)(S) : tau )
+ # f(S; tau) = int ( (dpsi / dS)(S) : tau )
+ #
+ # Notes:
+ # In general there should also be a b(dR, u, tau) term for tau symmetric,
+ # which will be zero if the deviatoric part of RF is zero
+ # w(dR, tau) is artificial inertia on dR (Tikhonov regularization)
+
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+
+ bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ RFk = fem.integrate(
+ self.rotated_defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "R": self.R, "tau": self.sym_test},
+ output_dtype=wp.vec3,
+ )
+ RBk = fem.integrate(
+ self.rotated_dispgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_trial, "R": self.R, "tau": self.sym_test},
+ output_dtype=float,
+ )
+
+ # Optional -- reset S as symmetric part of RFk
+ self.S.dof_values = self.W_inv @ RFk
+
+ Sk = fem.integrate(
+ tensor_mass_form,
+ # quadrature=self.quadrature,
+ nodal=True,
+ fields={"sig": self.S, "tau": self.sym_test},
+ output_dtype=wp.vec3,
+ )
+
+ # c_k -- sym part of constraint residual (RFk - WS)
+ c_k = RFk
+ array_axpy(Sk, c_k, alpha=-1.0, beta=1.0)
+
+ # Elasticity
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "sig": self.sym_trial, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec3,
+ )
+
+ # Schur complements
+
+ # lambda_rhs = H W^-1 c_k + f
+ lambda_rhs = f
+ bsr_mv(A=H, x=self.W_inv @ c_k, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ WiRBk = self.W_inv @ RBk
+ WiRBk_T = WiRBk.transpose()
+
+ bsr_mv(A=WiRBk_T, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix += WiRBk_T @ H @ WiRBk
+
+ # Rotation
+ RFk_skew = fem.integrate(
+ self.rotated_defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "R": self.R, "tau": self.skew_test},
+ output_dtype=float,
+ )
+
+ RBk_skew = fem.integrate(
+ self.rotated_dispgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_trial, "R": self.R, "tau": self.skew_test},
+ output_dtype=float,
+ )
+
+ # print(
+ # "RES ",
+ # wp.utils.array_inner(RFk_skew, bsr_mv(self.W_skew_inv, RFk_skew).view(dtype=RFk_skew.dtype)),
+ # wp.utils.array_inner(c_k, bsr_mv(self.W_inv, c_k).view(dtype=c_k.dtype)),
+ # )
+
+ Ck = fem.integrate(
+ self.defgrad_incremental_rotation_form,
+ fields={
+ "u": self.u_field,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.skew_test,
+ },
+ nodal=True,
+ output_dtype=float,
+ )
+ WiCt = self.rot_stiff * self.W_skew_inv @ Ck.transpose()
+
+ CWiCt_inv = Ck @ WiCt
+ fem_example_utils.invert_diagonal_bsr_matrix(CWiCt_inv)
+
+ RBk_skew_TCi = RBk_skew.transpose() @ CWiCt_inv
+ u_matrix += RBk_skew_TCi @ RBk_skew
+ bsr_mv(A=RBk_skew_TCi, x=RFk_skew, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # update S
+ # -RBk du + W dS = c_k
+ # W dS = c_k + RBK du
+ bsr_mv(A=RBk, x=delta_du, y=c_k, alpha=1.0, beta=1.0)
+ dS = self.W_inv @ c_k
+
+ # update R
+ #
+ # -RBk_skew du - CWiCt dlambda = RFk
+ # cW dR - Ct dlambda = 0
+ bsr_mv(A=RBk_skew, x=delta_du, y=RFk_skew, alpha=1.0, beta=1.0)
+ lambda_skew = -CWiCt_inv @ RFk_skew
+ dR = WiCt @ lambda_skew
+
+ return delta_du, dR, dS
+
+ @integrand
+ def constraint_residual(
+ s: Sample,
+ S: Field,
+ R: Field,
+ u: Field,
+ ):
+ """
+ Constraint residual
+ """
+ RFs = rotation_matrix(R(s)) * (grad(u, s) + wp.identity(n=2, dtype=float))
+ c = S(s) - RFs
+
+ return 0.5 * wp.ddot(c, c)
+
+ @integrand
+ def rotated_dispgrad_form(
+ s: Sample,
+ R: Field,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Rotated displacement gradient form
+ R grad(u) : tau
+ """
+ return wp.ddot(wp.transpose(tau(s)), rotation_matrix(R(s)) * grad(u, s))
+
+ @integrand
+ def rotated_defgrad_form(
+ s: Sample,
+ R: Field,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Rotated deformation gradient form
+ R (I + grad(u)) : tau
+ """
+ return wp.ddot(
+ wp.transpose(tau(s)),
+ rotation_matrix(R(s)) * (wp.identity(n=2, dtype=float) + grad(u, s)),
+ )
+
+ @integrand
+ def defgrad_incremental_rotation_form(
+ s: Sample, R: Field, dR: Field, u: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R dR grad(u) : tau
+ """
+ return wp.ddot(
+ rotation_matrix(R(s))
+ * dR(s)
+ * (wp.identity(n=2, dtype=float) + grad(u, s)),
+ wp.transpose(tau(s)),
+ )
+
+
+class MFEM_RS_F(MFEM):
+ """RS = F variant"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ # self.rot_stiff = self.rot_stiff / self.lame[0]
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces()
+
+ constraint_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ # dtype=wp.mat22,
+ dof_mapper=FullTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ self.constraint_test = fem.make_test(
+ space=constraint_space, space_partition=self.sym_test.space_partition
+ )
+ self.constraint_trial = fem.make_trial(
+ space=constraint_space, space_partition=self.sym_test.space_partition
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.quadrature = fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * (args.degree - 1)
+ )
+
+ u_sides = fem.make_trial(self.u_trial.space, domain=fem.Sides(self.geo))
+ tau_sides = fem.make_test(
+ self.constraint_test.space, domain=fem.Sides(self.geo)
+ )
+
+ self.B = fem.integrate(
+ self.dispgrad_form,
+ fields={"tau": self.constraint_test, "u": self.u_trial},
+ output_dtype=float,
+ ) + fem.integrate(
+ self.dispgrad_side_form,
+ fields={"tau": tau_sides, "u": u_sides},
+ output_dtype=float,
+ )
+
+ self.Bt = bsr_transposed(self.B)
+
+ self.W_skew = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.skew_test, "sig": self.skew_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_skew_inv = bsr_copy(self.W_skew)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_skew_inv)
+
+ def evaluate_ck(self):
+ tau_sides = fem.make_test(
+ self.constraint_test.space, domain=fem.Sides(self.geo)
+ )
+
+ Fk = fem.integrate(
+ self.defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+ fem.utils.array_axpy(
+ y=Fk,
+ x=fem.integrate(
+ self.dispgrad_side_form,
+ # quadrature=self.quadrature,
+ fields={"u": self.u_field.trace(), "tau": tau_sides},
+ output_dtype=wp.vec4,
+ ),
+ )
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ RSk = fem.integrate(
+ self.rotated_strain_form,
+ quadrature=self.quadrature,
+ fields={"sig": self.S, "R": self.R, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # c_k -- constraint residual (Fk - RS)
+ c_k = Fk
+ array_axpy(x=RSk, y=c_k, alpha=-1.0, beta=1.0)
+
+ ck_field = self.constraint_test.space.make_field()
+ ck_field.dof_values = c_k
+ return ck_field
+
+ def newton_iter(self, k: int):
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+ # bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ c_k = self.evaluate_ck().dof_values
+
+ # Grad of rotated strain w.r.t R, S
+ CSk = fem.integrate(
+ self.rotated_strain_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "R": self.R, "tau": self.constraint_test},
+ output_dtype=float,
+ )
+ CRk = fem.integrate(
+ self.incremental_strain_rotation_form,
+ nodal=True,
+ fields={
+ "sig": self.S,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.constraint_test,
+ },
+ output_dtype=float,
+ )
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={"S": self.S, "sig": self.sym_trial, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec3,
+ )
+
+ H_inv = bsr_copy(H)
+ wp.launch(
+ kernel=self.invert_hessian_blocks, dim=H_inv.nnz, inputs=[H_inv.values]
+ )
+
+ # Schur complements
+
+ CSkHi = CSk @ H_inv
+ CSt = CSk.transpose()
+
+ CHiCt = CSkHi @ CSt
+
+ # lambda_rhs = c_k + CS H^-1 f
+ lambda_rhs = c_k
+ bsr_mv(A=CSkHi, x=f, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ CRkWi = CRk @ (self.W_skew_inv * self.rot_stiff)
+ CRt = CRk.transpose()
+
+ CWiCt = CRkWi @ CRt
+
+ CHiCt_inv = bsr_copy(CHiCt, scalar_type=wp.float64) + bsr_copy(
+ CWiCt, scalar_type=wp.float64
+ )
+ wp.launch(
+ kernel=self.invert_schur_blocks,
+ dim=CHiCt_inv.nnz,
+ inputs=[CHiCt_inv.values],
+ )
+ CHiCt_inv = bsr_copy(CHiCt_inv, scalar_type=wp.float32)
+
+ BtCHiCt_inv = self.Bt @ CHiCt_inv
+
+ bsr_mm(x=BtCHiCt_inv, y=self.B, z=u_matrix, alpha=1.0, beta=1.0)
+ bsr_mv(A=BtCHiCt_inv, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ bsr_mv(A=self.B, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+ lambda_k = -CHiCt_inv @ lambda_rhs
+
+ # update S
+ # H dS + CSkT lambda = - f
+
+ bsr_mv(A=CSt, x=lambda_k, y=f, alpha=1.0, beta=1.0)
+ dS = -H_inv @ f
+
+ # update R
+ r = (CRt @ lambda_k).view(dtype=float)
+ dR = (-self.rot_stiff * self.W_skew_inv) @ r
+
+ return delta_du, dR, dS
+
+ @integrand
+ def constraint_residual(
+ domain: Domain,
+ s: Sample,
+ S: Field,
+ R: Field,
+ u: Field,
+ ):
+ """
+ Constraint residual
+ """
+ c = rotation_matrix(R(s)) * S(s) - (grad(u, s) + wp.identity(n=2, dtype=float))
+
+ return 0.5 * wp.ddot(c, c)
+
+ @integrand
+ def dispgrad_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau
+ """
+ return wp.ddot(tau(s), grad(u, s))
+
+ @integrand
+ def defgrad_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Deformation gradient form
+ (I + grad(u)) : tau
+ """
+ return wp.ddot(tau(s), (wp.identity(n=2, dtype=float) + grad(u, s)))
+
+ @integrand
+ def dispgrad_side_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau
+ """
+ grad_h = -wp.outer(fem.jump(u, s), fem.normal(domain, s))
+ return wp.ddot(tau(s), grad_h)
+
+ @integrand
+ def rotated_strain_form(
+ s: Sample, domain: Domain, R: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R S : tau
+ """
+ return wp.ddot(rotation_matrix(R(s)) * sig(s), tau(s))
+
+ @integrand
+ def incremental_strain_rotation_form(
+ s: Sample, domain: Domain, R: Field, dR: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R dR S : tau
+ """
+ return wp.ddot(rotation_matrix(R(s)) * dR(s) * sig(s), tau(s))
+
+ @wp.kernel
+ def invert_hessian_blocks(values: wp.array(dtype=(wp.mat33))):
+ i = wp.tid()
+ values[i] = wp.inverse(values[i])
+
+ @wp.kernel
+ def invert_schur_blocks(values: wp.array(dtype=(wp.mat44d))):
+ i = wp.tid()
+ values[i] = fem.utils.inverse_qr(values[i])
+
+ # @wp.kernel
+ # def pseudo_inverse_S(
+ # values: wp.array(dtype=(wp.mat(shape=(4, 3), dtype=wp.float32)))
+ # ):
+ # i = wp.tid()
+
+ # v = values[i]
+ # values[i] = v * wp.inverse(wp.transpose(v) * v)
+
+ # @wp.kernel
+ # def pseudo_inverse_R(
+ # values: wp.array(dtype=(wp.mat(shape=(4, 1), dtype=wp.float32)))
+ # ):
+ # i = wp.tid()
+
+ # v = values[i]
+ # values[i] = v / wp.ddot(v, v)
+
+ # @wp.kernel
+ # def add_coupling(
+ # CHCi: wp.array(dtype=(wp.mat(shape=(4, 4), dtype=wp.float32))),
+ # CS: wp.array(dtype=(wp.mat(shape=(4, 3), dtype=wp.float32))),
+ # CR: wp.array(dtype=(wp.mat(shape=(4, 1), dtype=wp.float32))),
+ # W: wp.array(dtype=float),
+ # rot_stiff: float
+ # ):
+ # i = wp.tid()
+
+ # cr = CR[i]
+ # cwc = cr * wp.transpose(cr) * rot_stiff / W[i]
+
+ # if i == 0:
+ # print(cwc)
+
+ # #cs = CS[i]
+ # #pis = cs * wp.transpose(cs) / wp.ddot(cs, cs)
+
+ # #CHCi[i] -= wp.ddot(pis, cwc) * pis
+ #
+ @wp.kernel
+ def compute_CHiCT_inv(
+ CHCi: wp.array(dtype=(wp.mat(shape=(4, 4), dtype=wp.float32))),
+ CS: wp.array(dtype=(wp.mat(shape=(4, 3), dtype=wp.float32))),
+ CR: wp.array(dtype=(wp.mat(shape=(4, 1), dtype=wp.float32))),
+ H: wp.array(dtype=(wp.mat(shape=(3, 3), dtype=wp.float32))),
+ W: wp.array(dtype=float),
+ rot_stiff: float,
+ ):
+ i = wp.tid()
+
+ cr = CR[i]
+ cs = CS[i]
+
+ pis = 3.0 * cs * wp.transpose(cs) / wp.ddot(cs, cs)
+ pir = wp.identity(n=4, dtype=wp.float32) - pis
+
+ cs_mp = cs * wp.inverse(wp.transpose(cs) * cs)
+ cihci = cs_mp * H[i] * wp.transpose(cs_mp)
+
+ cr_s = pis * cr
+ cr_r = cr - cr_s
+
+ wi = rot_stiff / W[i]
+
+ Ar = wi * cr_s * wp.transpose(cr_s)
+ A = cs * wp.inverse(H[i]) * wp.transpose(cs) + Ar
+ C = wi * cr_r * wp.transpose(cr_r)
+ B = wi * cr_r * wp.transpose(cr_s)
+
+ # A_mp = cihci - cihci * Ar * cihci
+ A_mp = pis * wp.inverse(A + pir) * pis
+
+ cr_mp = cr_r / wp.ddot(cr_r, cr_r)
+ C_mp = cr_mp * W[i] / rot_stiff * wp.transpose(cr_mp)
+
+ Ai = A_mp + A_mp * (wp.transpose(B) * C_mp * B) * A_mp
+ Ci = C_mp + C_mp * B * A_mp * wp.transpose(B) * C_mp
+
+ # AS = A - wp.transpose(B) * C_mp * B
+ # Ai = pis * wp.inverse(AS + pir) * pis
+
+ # CSsh = C - B * A_mp * wp.transpose(B)
+ # Ci = pir * wp.inverse(CSsh + pis) * pir
+
+ CHCi[i] = Ai + Ci - Ai * wp.transpose(B) * C_mp - Ci * B * A_mp
+
+ CHC = A + C + B + wp.transpose(B)
+ # CHCi[i] = wp.inverse(CHC)
+
+ if i == 0:
+ wp.print(CHCi[i] * CHC)
+
+
+class MFEM_S_RF_v2(MFEM):
+ """S = RF variant v2"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ # self.rot_stiff = self.rot_stiff / self.lame[0]
+
+ self.strain_form = tensor_mass_form
+ self.constraint_residual = MFEM_S_RF.constraint_residual
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces()
+
+ constraint_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ dof_mapper=FullTensorMapper(wp.mat22),
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ self.constraint_test = fem.make_test(
+ space=constraint_space, space_partition=self.sym_test.space_partition
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.quadrature = fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * (args.degree - 1)
+ )
+
+ self.W_skew = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.skew_test, "sig": self.skew_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_skew_inv = bsr_copy(self.W_skew)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_skew_inv)
+
+ def newton_iter(self, k: int):
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+ bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ # Deformation gradient Fl
+ RFk = fem.integrate(
+ MFEM_S_RF.rotated_defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "R": self.R, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+ RBk = fem.integrate(
+ MFEM_S_RF.rotated_dispgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_trial, "R": self.R, "tau": self.constraint_test},
+ output_dtype=float,
+ )
+ RBk_T = bsr_transposed(RBk)
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ Sk = fem.integrate(
+ self.strain_form,
+ # quadrature=self.quadrature,
+ nodal=True,
+ fields={"sig": self.S, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # c_k -- constraint residual (RFk - S)
+ c_k = RFk
+ array_axpy(x=Sk, y=c_k, alpha=-1.0, beta=1.0)
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={"S": self.S, "sig": self.sym_trial, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.sym_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec3,
+ )
+
+ # Grad of rotated strain w.r.t R, S
+ CSk = fem.integrate(
+ self.strain_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "tau": self.constraint_test},
+ output_dtype=float,
+ )
+ CRk = fem.integrate(
+ MFEM_S_RF.defgrad_incremental_rotation_form,
+ fields={
+ "u": self.u_field,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.constraint_test,
+ },
+ nodal=True,
+ output_dtype=float,
+ )
+
+ H_inv = bsr_copy(H)
+ wp.launch(
+ kernel=self.invert_hessian_blocks, dim=H_inv.nnz, inputs=[H_inv.values]
+ )
+
+ # Schur complements
+
+ CSkHi = bsr_mm(CSk, H_inv)
+ CSt = bsr_transposed(CSk)
+
+ CHiCt = bsr_mm(CSkHi, CSt)
+
+ # lambda_rhs = c_k + CS H^-1 f
+ lambda_rhs = c_k
+ bsr_mv(A=CSkHi, x=f, y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ CRkWi = bsr_mm(CRk, self.W_skew_inv, alpha=self.rot_stiff)
+ CRt = bsr_transposed(CRk)
+
+ CWiCt = bsr_mm(CRkWi, CRt, alpha=1.0, beta=1.0)
+
+ CHiCt_inv = bsr_axpy(
+ bsr_copy(CHiCt, scalar_type=wp.float64),
+ bsr_copy(CWiCt, scalar_type=wp.float64),
+ )
+ wp.launch(
+ kernel=self.invert_schur_blocks,
+ dim=CHiCt_inv.nnz,
+ inputs=[CHiCt_inv.values],
+ )
+ CHiCt_inv = bsr_copy(CHiCt_inv, scalar_type=wp.float32)
+
+ BtCHiCt_inv = bsr_mm(RBk_T, CHiCt_inv)
+
+ bsr_mm(x=BtCHiCt_inv, y=RBk, z=u_matrix, alpha=1.0, beta=1.0)
+ bsr_mv(A=BtCHiCt_inv, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # get back lambda
+ # -B du -ChiC lambda = lambda_k
+
+ bsr_mv(A=RBk, x=delta_du, y=lambda_rhs, alpha=1.0, beta=1.0)
+ lambda_k = bsr_mv(CHiCt_inv, lambda_rhs, alpha=-1.0, beta=0.0)
+
+ # update S
+ # H dS + CSkT lambda = - f
+
+ bsr_mv(A=CSt, x=lambda_k, y=f, alpha=1.0, beta=1.0)
+ dS = bsr_mv(A=H_inv, x=f, alpha=-1.0, beta=0.0)
+
+ # update R
+ r = bsr_mv(A=CRt, x=lambda_k).view(dtype=float)
+ dR = bsr_mv(A=self.W_skew_inv, x=r, alpha=self.rot_stiff, beta=0.0)
+
+ return delta_du, dR, dS
+
+ @wp.kernel
+ def invert_hessian_blocks(values: wp.array(dtype=(wp.mat33))):
+ i = wp.tid()
+ values[i] = wp.inverse(values[i])
+
+ @wp.kernel
+ def invert_schur_blocks(values: wp.array(dtype=(wp.mat44d))):
+ i = wp.tid()
+ values[i] = fem.utils.inverse_qr(values[i])
+
+
+class MFEM_S_F(MFEM):
+ """S = F variant"""
+
+ def __init__(self, args):
+ super().__init__(args)
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces()
+
+ constraint_space = fem.make_polynomial_space(
+ self.geo,
+ degree=self.strain_degree,
+ dtype=wp.mat22,
+ discontinuous=True,
+ element_basis=self.strain_basis,
+ family=self.strain_poly,
+ )
+
+ self.constraint_test = fem.make_test(space=constraint_space)
+ self.constraint_trial = fem.make_trial(space=constraint_space)
+
+ self.S = constraint_space.make_field()
+ self.S.dof_values.fill_(
+ wp.mat22(1.0, 0.0, 0.0, 1.0)
+ ) # initialize with identity
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.quadrature = fem.RegularQuadrature(
+ fem.Cells(self.geo), order=2 * args.degree - 1
+ )
+
+ self.B = fem.integrate(
+ self.dispgrad_form,
+ fields={"tau": self.constraint_test, "u": self.u_trial},
+ output_dtype=float,
+ )
+
+ self.W = fem.integrate(
+ tensor_mass_form,
+ fields={"tau": self.constraint_test, "sig": self.constraint_trial},
+ nodal=True,
+ output_dtype=float,
+ )
+ self.W_inv = bsr_copy(self.W)
+ fem_example_utils.invert_diagonal_bsr_matrix(self.W_inv)
+
+ self.WiB = self.W_inv @ self.B
+ self.BtWi = self.WiB.transpose()
+
+ def newton_iter(self, k: int):
+ # Unconstrained dynamics
+ A, l = self.assemble_constraint_free_system()
+
+ u_rhs = l
+ bsr_mv(A=self.A, x=self.du_field.dof_values, y=u_rhs, alpha=-1.0, beta=1.0)
+
+ u_matrix = bsr_copy(self.A)
+
+ # Deformation gradient Fl
+ Fk = fem.integrate(
+ self.defgrad_form,
+ quadrature=self.quadrature,
+ fields={"u": self.u_field, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # bsr_mv(self.W_inv, x=Fk, y=self.S.dof_values.view(dtype=wp.vec4), alpha=1.0, beta=0.0)
+
+ # Rotated deformation gradient RFk and gradient matrix RBk
+ Sk = fem.integrate(
+ tensor_mass_form,
+ quadrature=self.quadrature,
+ fields={"sig": self.S, "tau": self.constraint_test},
+ output_dtype=wp.vec4,
+ )
+
+ # c_k -- constraint residual (Fk - S)
+ c_k = Fk
+ array_axpy(x=Sk, y=c_k, alpha=-1.0, beta=1.0)
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ quadrature=self.quadrature,
+ fields={
+ "S": self.S,
+ "sig": self.constraint_test,
+ "tau": self.constraint_trial,
+ },
+ values={"lame": self.lame},
+ output_dtype=float,
+ )
+ f = fem.integrate(
+ self.elastic_gradient_form,
+ quadrature=self.quadrature,
+ fields={"S": self.S, "tau": self.constraint_test},
+ values={"lame": self.lame},
+ output_dtype=wp.vec4,
+ )
+
+ # Schur complement
+
+ # lambda_rhs = H W^-1 c_k + f
+ lambda_rhs = f
+ bsr_mv(A=H, x=bsr_mv(self.W_inv, c_k), y=lambda_rhs, alpha=1.0, beta=1.0)
+
+ bsr_mv(A=self.BtWi, x=lambda_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+ bsr_mm(x=self.BtWi, y=bsr_mm(H, self.WiB), z=u_matrix, alpha=1.0, beta=1.0)
+
+ # Enforce boundary conditions
+ du_bd_rhs = wp.clone(self.v_bd_rhs)
+ bsr_mv(
+ A=self.v_bd_matrix,
+ x=self.du_field.dof_values,
+ y=du_bd_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ )
+ fem.project_linear_system(
+ u_matrix, u_rhs, self.v_bd_matrix, du_bd_rhs, normalize_projector=False
+ )
+
+ delta_du = wp.zeros_like(u_rhs)
+ fem_example_utils.bsr_cg(
+ u_matrix, b=u_rhs, x=delta_du, tol=1.0e-16, max_iters=250, quiet=True
+ )
+
+ # update S
+ # -Bk du + W dS = c_k
+ # W dS = c_k + RBK du
+ bsr_mv(A=self.B, x=delta_du, y=c_k, alpha=1.0, beta=1.0)
+ dS = bsr_mv(A=self.W_inv, x=c_k, alpha=1.0, beta=0.0)
+ dS = dS.view(dtype=wp.mat22)
+
+ dR = self.skew_test.space.make_field().dof_values
+
+ return delta_du, dR, dS
+
+ @integrand
+ def constraint_residual(
+ s: Sample,
+ S: Field,
+ R: Field,
+ u: Field,
+ ):
+ """
+ Constraint residual
+ """
+ c = S(s) - (grad(u, s) + wp.identity(n=2, dtype=float))
+ return 0.5 * wp.ddot(c, c)
+
+ @integrand
+ def dispgrad_form(
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau
+ """
+ return wp.ddot(tau(s), grad(u, s))
+
+ @integrand
+ def defgrad_form(
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Deformation gradient form
+ (I + grad(u)) : tau
+ """
+ return wp.ddot(tau(s), (wp.identity(n=2, dtype=float) + grad(u, s)))
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+ wp.set_module_options({"enable_backward": False})
+ wp.set_module_options({"max_unroll": 2})
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument("--degree", type=int, default=1)
+ parser.add_argument("--serendipity", action="store_true", default=False)
+ parser.add_argument("--displacement", type=float, default=0.0)
+ parser.add_argument("-n", "--n_frames", type=int, default=25)
+ parser.add_argument("--n_newton", type=int, default=2)
+ parser.add_argument("--n_backtrack", type=int, default=4)
+ parser.add_argument("--young_modulus", type=float, default=100.0)
+ parser.add_argument("--poisson_ratio", type=float, default=0.5)
+ parser.add_argument("--gravity", type=float, default=10.0)
+ parser.add_argument("--density", type=float, default=1.0)
+ parser.add_argument("--dt", type=float, default=0.1)
+ parser.add_argument("--rot_compliance", type=float, default=0.001)
+ parser.add_argument("-v", "--variant", type=str, default="rs_f")
+ parser.add_argument("--grid", action="store_true", default=False)
+ parser.add_argument("-nh", "--neo_hookean", action="store_true", default=False)
+ args = parser.parse_args()
+
+ if args.variant == "rs_f":
+ sim = MFEM_RS_F(args)
+ elif args.variant == "s_rf":
+ sim = MFEM_S_RF(args)
+ elif args.variant == "s_rf2":
+ sim = MFEM_S_RF_v2(args)
+ elif args.variant == "s_f":
+ sim = MFEM_S_F(args)
+ else:
+ raise ValueError(f"Invalid variant: {args.variant}")
+
+ sim.init_vel_space()
+ sim.init_strain_spaces()
+ sim.init_boundary_conditions()
+ sim.init_constant_forms()
+
+ plot = fem_example_utils.Plot()
+ plot.add_field("u", sim.u_field)
+
+ for f in range(args.n_frames):
+ # TODO currently performing a single Newton iteration
+
+ print(f"--- Frame {f} ---")
+ sim.run_frame()
+ plot.add_field("u", sim.u_field)
+
+ plot.plot(
+ {
+ "u": {
+ "displacement": {},
+ "xlim": (-0.5, 1.5),
+ "ylim": (-0.25, 1.25),
+ },
+ },
+ backend="matplotlib",
+ )
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/mfem_3d.py b/deps/vomp/vomp/fem/fem_examples/mfem/mfem_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..d263cd7ff75d0d468c7d6c4a6b265bc6bbaf0810
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/mfem_3d.py
@@ -0,0 +1,1332 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import math
+import gc
+from typing import Any, Optional
+
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+from warp.fem import Domain, Field, Sample
+from warp.fem.utils import array_axpy
+
+from fem_examples.mfem.linalg import MFEMSystem
+from fem_examples.mfem.softbody_sim import (
+ SoftbodySim,
+ defgrad,
+)
+
+from fem_examples.mfem.elastic_models import hooke_energy, hooke_stress, hooke_hessian
+from fem_examples.mfem.elastic_models import (
+ symmetric_strain,
+ symmetric_strain_delta,
+ snh_energy,
+ snh_stress,
+ snh_hessian_proj,
+)
+
+wp.set_module_options({"enable_backward": False})
+wp.set_module_options({"max_unroll": 4})
+wp.set_module_options({"fast_math": True})
+
+Scalar = wp.float32
+vec3s = wp.vec(length=3, dtype=Scalar)
+vec6s = wp.vec(length=6, dtype=Scalar)
+vec9s = wp.vec(length=9, dtype=Scalar)
+
+_SQRT_2 = wp.constant(math.sqrt(2.0))
+_SQRT_1_2 = wp.constant(math.sqrt(1.0 / 2.0))
+
+
+class FullTensorMapper(fem.DofMapper):
+ """Orthonormal isomorphism from R^{n (n+1)} to nxn symmetric tensors,
+ using usual L2 norm for vectors and half Frobenius norm, (tau : tau)/2 for tensors.
+ """
+
+ def __init__(self):
+ self.value_dtype = wp.mat33
+ self.DOF_SIZE = wp.constant(9)
+ self.dof_dtype = vec9s
+
+ def __str__(self):
+ return f"_{self.DOF_SIZE}"
+
+ @wp.func
+ def dof_to_value(dof: vec9s):
+ a = _SQRT_2 * dof[0]
+ b = _SQRT_2 * dof[1]
+ c = _SQRT_2 * dof[2]
+ d = dof[3]
+ e = dof[4]
+ f = dof[5]
+
+ ka = dof[6]
+ kb = dof[7]
+ kc = dof[8]
+ return wp.mat33(
+ a,
+ f - kc,
+ e + kb,
+ f + kc,
+ b,
+ d - ka,
+ e - kb,
+ d + ka,
+ c,
+ )
+
+ @wp.func
+ def value_to_dof(val: wp.mat33):
+ a = _SQRT_1_2 * val[0, 0]
+ b = _SQRT_1_2 * val[1, 1]
+ c = _SQRT_1_2 * val[2, 2]
+
+ d = 0.5 * (val[2, 1] + val[1, 2])
+ e = 0.5 * (val[0, 2] + val[2, 0])
+ f = 0.5 * (val[1, 0] + val[0, 1])
+
+ ka = 0.5 * (val[2, 1] - val[1, 2])
+ kb = 0.5 * (val[0, 2] - val[2, 0])
+ kc = 0.5 * (val[1, 0] - val[0, 1])
+
+ return vec9s(a, b, c, d, e, f, ka, kb, kc)
+
+
+@wp.func
+def rotation_matrix(rot_vec: wp.vec3):
+ quat = wp.quat_from_axis_angle(wp.normalize(rot_vec), wp.length(rot_vec))
+ return wp.quat_to_matrix(quat)
+
+
+@wp.kernel
+def apply_rotation_delta(
+ r_vec: wp.array(dtype=wp.vec3), dR: wp.array(dtype=wp.vec3), alpha: float
+):
+ i = wp.tid()
+
+ Q = wp.quat_from_axis_angle(wp.normalize(r_vec[i]), wp.length(r_vec[i]))
+ omega = dR[i] * alpha
+
+ dQ = wp.quat(omega, 0.0)
+ Q = wp.normalize(Q + 0.5 * Q * dQ)
+
+ axis = wp.vec3()
+ angle = float(0)
+ wp.quat_to_axis_angle(Q, axis, angle)
+ r_vec[i] = axis * angle
+
+
+@fem.integrand
+def tensor_mass_form(s: Sample, sig: Field, tau: Field):
+ """
+ Mass form over tensor space
+ sig : tau
+ """
+ return wp.ddot(sig(s), tau(s))
+
+
+class LineSearchMeritCriterion:
+ # Numeric Optimization, chapter 15.4
+
+ def __init__(self, sim: SoftbodySim):
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ delta_u, dS, dR, dLambda = delta_fields
+
+ c_k = rhs[3]
+ c_k_normalized = wp.empty_like(c_k)
+
+ wp.launch(
+ self._normalize_c_k,
+ inputs=[c_k, c_k_normalized, sim._stiffness_field.dof_values],
+ dim=c_k.shape,
+ )
+
+ delta_ck = lhs._B @ delta_u
+ sp.bsr_mv(A=lhs._Cs, x=dS, y=delta_ck, alpha=-1.0, beta=1.0)
+
+ if lhs._Cr is not None:
+ sp.bsr_mv(A=lhs._Cr, x=dR, y=delta_ck, alpha=-1.0, beta=1.0)
+
+ m = wp.utils.array_inner(dS, sim._dE_dS.view(dS.dtype)) - wp.utils.array_inner(
+ delta_u, sim._minus_dE_du.view(delta_u.dtype)
+ ) * wp.utils.array_inner(c_k_normalized, delta_ck.view(c_k_normalized.dtype))
+
+ self.m = m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ f_cur = E_cur + C_cur
+ f_ref = E_ref + C_ref
+
+ return f_cur <= f_ref + self.armijo_coeff * alpha * self.m
+
+ @wp.kernel
+ def _normalize_c_k(
+ c_k: wp.array(dtype=Any),
+ c_k_norm: wp.array(dtype=Any),
+ scale: wp.array(dtype=float),
+ ):
+ i = wp.tid()
+ c_k_norm[i] = wp.normalize(c_k[i]) * scale[i]
+
+
+class LineSearchMultiObjCriterion:
+ # Line Search Filter Methods for Nonlinear Programming: Motivation and Global Convergence
+ # 2005, SIAM Journal on Optimization 16(1):1-31
+
+ def __init__(self, sim: SoftbodySim):
+ # constraint decrease
+ E_scale = sim.typical_stiffness / sim.lame_ref[1]
+ self.gamma_theta = 0.75
+ self.gamma_f = 0.1 * E_scale
+
+ # switching rule
+ self.s_theta = 1.5
+ self.s_rho = 2.5 * self.s_theta
+ self.delta = 0.01 * E_scale ** (self.s_theta / self.s_rho)
+
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ delta_u, dS, dR, dLambda = delta_fields
+ m = wp.utils.array_inner(dS, sim._dE_dS.view(dS.dtype)) - wp.utils.array_inner(
+ delta_u, sim._minus_dE_du.view(delta_u.dtype)
+ )
+ self.m = m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ if (
+ self.m < 0.0
+ and (-self.m) ** self.s_rho * alpha > self.delta * C_ref**self.s_theta
+ ):
+ return E_cur <= E_ref + self.armijo_coeff * alpha * self.m
+
+ return C_cur <= (1.0 - self.gamma_theta) * C_ref or (
+ E_cur <= E_ref - self.gamma_f * C_ref
+ )
+
+
+class LineSearchLagrangianArmijoCriterion:
+ # Unconstrained line-search based on Lagrangian
+
+ def __init__(self, sim: SoftbodySim):
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ delta_u, dS, dR, dLambda = delta_fields
+
+ m = wp.utils.array_inner(dS, sim._dE_dS.view(dS.dtype)) - wp.utils.array_inner(
+ delta_u, sim._minus_dE_du.view(delta_u.dtype)
+ )
+
+ c_k = rhs[3]
+ delta_ck = lhs._B @ delta_u
+ sp.bsr_mv(A=lhs._Cs, x=dS, y=delta_ck, alpha=-1.0, beta=1.0)
+ if lhs._Cr is not None:
+ sp.bsr_mv(A=lhs._Cr, x=dR, y=delta_ck, alpha=-1.0, beta=1.0)
+
+ c_m = wp.utils.array_inner(c_k, dLambda.view(c_k.dtype)) + wp.utils.array_inner(
+ delta_ck, sim.constraint_field.dof_values.view(delta_ck.dtype)
+ )
+ self.m = m - c_m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ return E_cur + C_cur <= E_ref + C_ref + self.armijo_coeff * alpha * self.m
+
+
+class MFEM(SoftbodySim):
+ def __init__(self, geo: fem.Geometry, active_cells: wp.array, args):
+ super().__init__(geo, active_cells, args)
+
+ self._make_elasticity_forms()
+ self._init_strain_basis()
+
+ self._lagrangian_constraint_energy = False
+ if self.args.line_search == "merit":
+ self._ls = LineSearchMeritCriterion(self)
+ elif self.args.line_search == "lagrangian":
+ self._lagrangian_constraint_energy = True
+ self._ls = LineSearchLagrangianArmijoCriterion(self)
+ else:
+ self._ls = LineSearchMultiObjCriterion(self)
+
+ # Temp storage for energy cuda graph
+ self._E = wp.empty(3, dtype=wp.float64)
+ self._E_pinned = wp.empty_like(self._E, device="cpu", pinned=True)
+ self._E_graph = None
+
+ def set_strain_basis(self, strain_basis: Optional[fem.BasisSpace]):
+ if strain_basis is None:
+ self._init_strain_basis()
+ else:
+ self._strain_basis = strain_basis
+
+ def _make_elasticity_forms(self):
+ if self.args.neo_hookean:
+ self.elastic_energy_form = MFEM.nh_elasticity_energy_form
+ self.elastic_gradient_form = MFEM.nh_elasticity_gradient_form
+ self.elastic_hessian_form = MFEM.nh_elasticity_hessian_form
+ else:
+ self.elastic_energy_form = MFEM.hooke_elasticity_energy_form
+ self.elastic_gradient_form = MFEM.hooke_elasticity_gradient_form
+ self.elastic_hessian_form = MFEM.hooke_elasticity_hessian_form
+
+ def _init_strain_basis(self):
+ if isinstance(self.geo.reference_cell(), fem.geometry.element.Cube):
+ strain_degree = self.args.degree
+ strain_basis = fem.ElementBasis.LAGRANGE
+ strain_poly = fem.Polynomial.GAUSS_LEGENDRE
+ else:
+ strain_degree = self.args.degree - 1
+ strain_basis = fem.ElementBasis.NONCONFORMING_POLYNOMIAL
+ strain_poly = None
+
+ self._strain_basis = fem.make_polynomial_basis_space(
+ self.geo,
+ degree=strain_degree,
+ discontinuous=True,
+ element_basis=strain_basis,
+ family=strain_poly,
+ )
+
+ def init_strain_spaces(self, constraint_dof_mapper: fem.DofMapper):
+ sym_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dof_mapper=fem.SymmetricTensorMapper(
+ wp.mat33, mapping=fem.SymmetricTensorMapper.Mapping.DB16
+ ),
+ )
+
+ # Function spaces for piecewise-constant per-element rotations and rotation vectors
+ rot_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dtype=wp.vec3,
+ )
+ skew_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dof_mapper=fem.SkewSymmetricTensorMapper(wp.mat33),
+ )
+ constraint_space = fem.make_collocated_function_space(
+ self._strain_basis,
+ dof_mapper=constraint_dof_mapper,
+ )
+
+ strain_space_partition = fem.make_space_partition(
+ space_topology=self._strain_basis.topology,
+ geometry_partition=self._geo_partition,
+ with_halo=False,
+ )
+
+ # Defines some fields over our function spaces
+ self.S = sym_space.make_field(
+ space_partition=strain_space_partition
+ ) # Rotated symmetric train
+ self.S.dof_values.fill_(
+ sym_space.dof_mapper.value_to_dof(
+ wp.mat33(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
+ )
+ ) # initialize with identity
+
+ self.R = rot_space.make_field(
+ space_partition=strain_space_partition
+ ) # Rotation
+
+ self.constraint_field = constraint_space.make_field(
+ space_partition=strain_space_partition
+ )
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ domain = self.u_test.domain
+ self.sym_test = fem.make_test(
+ space=sym_space, space_partition=strain_space_partition, domain=domain
+ )
+ self.sym_trial = fem.make_trial(
+ space=sym_space, space_partition=strain_space_partition, domain=domain
+ )
+
+ self.skew_test = fem.make_test(
+ space=skew_space, space_partition=strain_space_partition, domain=domain
+ )
+ self.skew_trial = fem.make_trial(
+ space=skew_space, space_partition=strain_space_partition, domain=domain
+ )
+
+ self.constraint_test = fem.make_test(
+ space=constraint_space, space_partition=strain_space_partition
+ )
+
+ # self.strain_quadrature = fem.RegularQuadrature(self.sym_test.domain, order=2 * sym_space.degree)
+ # self.elasticity_quadrature = fem.RegularQuadrature(self.sym_test.domain, order=2 * sym_space.degree)
+ self.strain_quadrature = fem.NodalQuadrature(
+ self.sym_test.domain, space=sym_space
+ )
+ self.elasticity_quadrature = fem.NodalQuadrature(
+ self.sym_test.domain, space=sym_space
+ )
+
+ self._stiffness_field = fem.make_collocated_function_space(
+ self._strain_basis, dtype=float
+ ).make_field()
+ fem.interpolate(
+ MFEM._typical_stiffness_field,
+ fields={"lame": self.lame_field},
+ dest=self._stiffness_field,
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ # Temp storage so we can use cuda graphs (forward pass only)
+ self._u_rhs = wp.empty_like(self.u_field.dof_values, requires_grad=False)
+ self._f = wp.empty_like(self.S.dof_values, requires_grad=False)
+ self._w = wp.empty_like(self.R.dof_values, requires_grad=False)
+ self._c_k = wp.empty_like(self.constraint_field.dof_values, requires_grad=False)
+ self._rhs_graph = None
+
+ # For line search
+ self._minus_dE_du = wp.empty_like(self._u_rhs)
+ self._dE_dS = wp.empty_like(self._f)
+ self._dE_dR = wp.empty_like(self._w)
+
+ self._schur_work_arrays = None
+
+ def reset_fields(self):
+ super().reset_fields()
+
+ self.S.dof_values.fill_(
+ self.S.space.dof_mapper.value_to_dof(
+ wp.mat33(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
+ )
+ ) # initialize with identity
+ self.R.dof_values.zero_()
+ self.constraint_field.dof_values.zero_()
+
+ def checkpoint_newton_values(self):
+ super().checkpoint_newton_values()
+
+ self._R_cur = wp.clone(self.R.dof_values)
+ self._S_cur = wp.clone(self.S.dof_values)
+ self._lbd_cur = wp.clone(self.constraint_field.dof_values)
+
+ def apply_newton_deltas(self, delta_fields, alpha=1.0):
+ super().apply_newton_deltas(delta_fields, alpha=alpha)
+
+ wp.copy(src=self._S_cur, dest=self.S.dof_values)
+ wp.copy(src=self._lbd_cur, dest=self.constraint_field.dof_values)
+
+ _, dS, dR, dLambda = delta_fields
+
+ array_axpy(x=dS, y=self.S.dof_values, alpha=alpha)
+
+ array_axpy(
+ x=dLambda.view(dtype=self.constraint_field.dof_values.dtype),
+ y=self.constraint_field.dof_values,
+ alpha=alpha,
+ )
+
+ self._apply_rotation_delta(dR, alpha)
+
+ def _apply_rotation_delta(self, dR, alpha):
+ wp.copy(src=self._R_cur, dest=self.R.dof_values)
+ wp.launch(
+ apply_rotation_delta, dim=dR.shape[0], inputs=[self.R.dof_values, dR, alpha]
+ )
+
+ def _evaluate_energy(self, E_u, E_e, c_r, lagrangian=False):
+ super().evaluate_energy(E_u=E_u)
+
+ fem.integrate(
+ self.elastic_energy_form,
+ quadrature=self.elasticity_quadrature,
+ fields={"S": self.S, "lame": self.lame_field},
+ output=E_e,
+ )
+
+ if lagrangian:
+ fem.integrate(
+ self.constraint_form,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u": self.u_field,
+ "sig": self.S,
+ "R": self.R,
+ "tau": self.constraint_field,
+ },
+ kernel_options={"enable_backward": True},
+ output=c_r,
+ )
+ else:
+ c_k = wp.empty_like(self.constraint_field.dof_values, requires_grad=False)
+ self._evaluate_constraint_residual(out=c_k)
+ c_k_norm = wp.empty(shape=c_k.shape, dtype=wp.float64)
+ wp.launch(
+ MFEM.constraint_norm,
+ dim=c_k.shape,
+ inputs=[c_k, c_k_norm, self._stiffness_field.dof_values],
+ )
+ wp.utils.array_sum(c_k_norm, out=c_r)
+
+ def evaluate_energy(self):
+ E_u = self._E[0:1]
+ E_e = self._E[1:2]
+ c_r = self._E[2:3]
+
+ lagrangian = self._lagrangian_constraint_energy
+
+ if (
+ self.args.cuda_graphs
+ and self.__class__._ENERGY_MODULES_LOADED
+ and self._E_graph is None
+ ):
+ try:
+ gc.collect(0)
+ gc.disable()
+ with wp.ScopedCapture(force_module_load=False) as capture:
+ self._evaluate_energy(E_u, E_e, c_r, lagrangian=lagrangian)
+ wp.copy(src=self._E, dest=self._E_pinned)
+ gc.collect(0)
+ gc.enable()
+ self._E_graph = capture.graph
+ except Exception as err:
+ print("Energy graph capture failed", err)
+
+ if self._E_graph is None:
+ self._evaluate_energy(E_u, E_e, c_r, lagrangian=lagrangian)
+ wp.copy(src=self._E, dest=self._E_pinned)
+ self.__class__._ENERGY_MODULES_LOADED = True
+ else:
+ wp.capture_launch(self._E_graph)
+
+ wp.synchronize_stream()
+
+ E = self._E_pinned.numpy()
+ E_tot = E[0] + E[1]
+ c_r = -E[2] if lagrangian else E[2]
+
+ return E_tot, c_r
+
+ def solve_newton_system(self, lhs, rhs):
+ if self.args.fp64:
+ lhs = lhs.cast(wp.float64)
+
+ if self._schur_work_arrays is None:
+ self._schur_work_arrays = sp.bsr_mm_work_arrays()
+ reuse_topology = False
+ else:
+ reuse_topology = True
+
+ return lhs.solve_schur(
+ rhs,
+ max_iters=self.args.cg_iters,
+ tol=self.args.cg_tol,
+ work_arrays=self._schur_work_arrays,
+ reuse_topology=reuse_topology,
+ )
+
+ def newton_rhs(self, tape: wp.Tape = None):
+ with_gradient = tape is not None
+
+ u_rhs = self.constraint_free_rhs(tape=tape)
+
+ if with_gradient:
+ c_k = wp.empty_like(self.constraint_field.dof_values, requires_grad=True)
+ f = wp.empty_like(self.S.dof_values, requires_grad=True)
+ w = wp.empty_like(self.R.dof_values, requires_grad=True)
+
+ with tape:
+ self._assemble_rhs(u_rhs, f, w, c_k)
+ else:
+ wp.copy(src=u_rhs, dest=self._u_rhs)
+ u_rhs = self._u_rhs
+ f = self._f
+ w = self._w
+ c_k = self._c_k
+
+ if (
+ self.args.cuda_graphs
+ and self.__class__._RHS_MODULES_LOADED
+ and self._rhs_graph is None
+ ):
+ try:
+ gc.collect(0)
+ gc.disable()
+ with wp.ScopedCapture(force_module_load=False) as capture:
+ self._assemble_rhs(
+ u_rhs,
+ f,
+ w,
+ c_k,
+ minus_dE_dU=self._minus_dE_du,
+ dE_dS=self._dE_dS,
+ dE_dR=self._dE_dR,
+ )
+ gc.collect(0)
+ gc.enable()
+ self._rhs_graph = capture.graph
+ except Exception as err:
+ print("RHS CAPTURE FAILED", err)
+
+ if self._rhs_graph is None:
+ self._assemble_rhs(
+ u_rhs,
+ f,
+ w,
+ c_k,
+ minus_dE_dU=self._minus_dE_du,
+ dE_dS=self._dE_dS,
+ dE_dR=self._dE_dR,
+ )
+ self.__class__._RHS_MODULES_LOADED = True
+ else:
+ wp.capture_launch(self._rhs_graph)
+
+ # Displacement boundary condition -- Filter u rhs
+ self._filter_forces(u_rhs, tape=tape)
+
+ return u_rhs, f, w, c_k
+
+ def record_adjoint(self, tape):
+ # The forward Newton is finding a root of rhs(q, p) = 0 with q = (u, S, R, lambda)
+ # so drhs/dp = drhs/dq dq/dp + drhs/dp = 0
+ # [- drhs/dq] dq/dp = drhs/dp
+ # lhs dq/dp = drhs/dp
+
+ self.prepare_newton_step(tape)
+ rhs = self.newton_rhs(tape=tape)
+ lhs = self.newton_lhs()
+
+ def solve_backward():
+ adj_res = (
+ self.u_field.dof_values.grad,
+ *(wp.zeros_like(field) for field in rhs[1:-1]),
+ self.constraint_field.dof_values.grad,
+ )
+ delta_du, dS, dR, dLambda = lhs.cast(wp.float64).solve_schur(
+ adj_res, max_iters=self.args.cg_iters
+ )
+
+ u_rhs, f, w_lambda, c_k = rhs
+ wp.copy(src=delta_du, dest=u_rhs.grad)
+ wp.copy(src=dLambda, dest=c_k.grad)
+ array_axpy(dS, f.grad, alpha=-1.0, beta=0.0)
+ array_axpy(dR, w_lambda.grad, alpha=-1.0, beta=0.0)
+
+ tape.record_func(
+ solve_backward,
+ arrays=[
+ self.u_field.dof_values,
+ self.constraint_field.dof_values,
+ *rhs,
+ ],
+ )
+
+ def interpolate_constraint_field(self, strain=False):
+ tau = fem.make_test(
+ self.interpolated_constraint_field.space,
+ space_restriction=self.u_test.space_restriction,
+ )
+
+ if strain:
+ # interpolate strain instead of stress field
+ fem.integrate(
+ tensor_mass_form,
+ quadrature=self.strain_quadrature,
+ fields={"sig": self.S, "tau": tau},
+ output=self.interpolated_constraint_field.dof_values,
+ kernel_options={"enable_backward": True},
+ )
+ else:
+ fem.integrate(
+ tensor_mass_form,
+ quadrature=self.strain_quadrature,
+ fields={"sig": self.constraint_field, "tau": tau},
+ output=self.interpolated_constraint_field.dof_values,
+ kernel_options={"enable_backward": True},
+ )
+
+ # Scale by inverse mass
+
+ if self._mass is None:
+ mass_test = fem.make_test(
+ self._mass_space, space_partition=self.u_test.space_partition
+ )
+ self._mass = fem.integrate(
+ self.mass_form,
+ quadrature=self.strain_quadrature,
+ fields={"p": mass_test},
+ output_dtype=wp.float32,
+ )
+
+ wp.launch(
+ self.scale_interpolated_quantity,
+ dim=self._mass.shape,
+ inputs=[self.interpolated_constraint_field.dof_values, self._mass],
+ )
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ super(MFEM, MFEM).add_parser_arguments(parser)
+
+ parser.add_argument(
+ "--cuda-graphs", action=argparse.BooleanOptionalAction, default=True
+ )
+ parser.add_argument(
+ "--line-search",
+ "-ls",
+ choices=["merit", "mobj", "lagrangian"],
+ default="merit",
+ )
+
+ @fem.integrand
+ def mass_form(s: Sample, p: Field):
+ return p(s)
+
+ @wp.kernel
+ def scale_interpolated_quantity(
+ qtt: wp.array(dtype=wp.mat33), mass: wp.array(dtype=float)
+ ):
+ i = wp.tid()
+ qtt[i] = qtt[i] / mass[i]
+
+ @fem.integrand
+ def _typical_stiffness_field(s: Sample, lame: Field):
+ return wp.min(lame(s))
+
+ @fem.integrand
+ def hooke_elasticity_hessian_form(
+ s: Sample, domain: Domain, S: Field, tau: Field, sig: Field, lame: Field
+ ):
+ return hooke_hessian(S(s), tau(s), sig(s), lame(s))
+
+ @fem.integrand
+ def hooke_elasticity_gradient_form(
+ s: Sample, domain: Domain, tau: Field, S: Field, lame: Field
+ ):
+ return wp.ddot(tau(s), hooke_stress(S(s), lame(s)))
+
+ @fem.integrand
+ def hooke_elasticity_energy_form(s: Sample, domain: Domain, S: Field, lame: Field):
+ return hooke_energy(S(s), lame(s))
+
+ @fem.integrand
+ def nh_elasticity_hessian_form(
+ s: Sample, domain: Domain, S: Field, tau: Field, sig: Field, lame: Field
+ ):
+ return snh_hessian_proj(S(s), tau(s), sig(s), lame(s))
+
+ @fem.integrand
+ def nh_elasticity_gradient_form(
+ s: Sample, domain: Domain, tau: Field, S: Field, lame: Field
+ ):
+ return wp.ddot(tau(s), snh_stress(S(s), lame(s)))
+
+ @fem.integrand
+ def nh_elasticity_energy_form(s: Sample, domain: Domain, S: Field, lame: Field):
+ return snh_energy(S(s), lame(s))
+
+ @wp.kernel
+ def constraint_norm(
+ C: wp.array(dtype=Any),
+ C_norm: wp.array(dtype=wp.float64),
+ scale: wp.array(dtype=wp.float32),
+ ):
+ i = wp.tid()
+ Ci = C[i]
+ C_norm[i] = wp.float64(wp.sqrt(0.5 * wp.dot(Ci, Ci)) * scale[i])
+
+
+class MFEM_RS_F(MFEM):
+ """RS = F variant"""
+
+ _RHS_MODULES_LOADED = False
+ _ENERGY_MODULES_LOADED = False
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces(constraint_dof_mapper=FullTensorMapper())
+
+ self._pen_field = fem.make_collocated_function_space(
+ self._strain_basis,
+ dtype=wp.vec2,
+ ).make_field(space_partition=self.sym_test.space_partition)
+ self._pen_field_restr = fem.make_restriction(
+ self._pen_field, space_restriction=self.sym_test.space_restriction
+ )
+
+ def supports_discontinuities(self):
+ return True
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ if self.has_discontinuities():
+ self._constraint_side_test = fem.make_test(
+ self.constraint_test.space,
+ space_partition=self.constraint_test.space_partition,
+ domain=fem.Sides(self.geo),
+ )
+
+ # Displacement gradient matrix
+ self.B = fem.integrate(
+ self.dispgrad_form,
+ fields={"tau": self.constraint_test, "u": self.u_trial},
+ output_dtype=float,
+ quadrature=self.strain_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self.has_discontinuities():
+ self.B += fem.integrate(
+ self.dispgrad_side_form,
+ fields={"tau": self._constraint_side_test, "u": self.u_side_trial},
+ output_dtype=float,
+ quadrature=self.side_quadrature,
+ )
+ self.B.nnz_sync()
+
+ # Temp storage for lhs cuda graph
+ self._lhs = None
+ self._lhs_graph = None
+
+ def project_constant_forms(self):
+ super().project_constant_forms()
+
+ self.B_proj = sp.bsr_copy(self.B)
+ sp.bsr_mm(x=self.B, y=self.v_bd_matrix, z=self.B_proj, alpha=-1.0, beta=1.0)
+ self.B_proj.nnz_sync()
+
+ self.Bt_proj = sp.bsr_transposed(self.B_proj)
+ self.Bt_proj.nnz_sync()
+
+ def prepare_newton_step(self, tape: Optional[wp.Tape] = None):
+ # Update penalization (no contribution to derivatives)
+ backward_step = tape is not None
+ fem.interpolate(
+ self.penalization_field,
+ dest=self._pen_field_restr,
+ fields={
+ "S": self.S,
+ "R": self.R,
+ "stress": self.constraint_field,
+ "lame": self.lame_field,
+ },
+ values={
+ "rot_compliance": 0.0 if backward_step else self.args.rot_compliance,
+ "typ_stiff": self.typical_stiffness * self.args.constraint_pen,
+ },
+ )
+
+ def newton_lhs(self):
+ if self.args.cuda_graphs and self._lhs is not None and self._lhs_graph is None:
+ try:
+ gc.collect(0)
+ gc.disable()
+ with wp.ScopedCapture(force_module_load=False) as capture:
+ self._assemble_lhs(self._lhs)
+ gc.collect(0)
+ gc.enable()
+ self._lhs_graph = capture.graph
+ except Exception as err:
+ print("LHS capture failed", err)
+
+ if self._lhs_graph is None:
+ if self._lhs is None:
+ self._lhs = self._assemble_lhs()
+ self._lhs._H_pen = sp.bsr_copy(self._lhs._H)
+ else:
+ self._assemble_lhs(self._lhs)
+ else:
+ wp.capture_launch(self._lhs_graph)
+
+ self._lhs._A = self.A_proj
+ self._lhs._B = self.B_proj
+ self._lhs._Bt = self.Bt_proj
+
+ return self._lhs
+
+ def _assemble_lhs(self, lhs: MFEMSystem = None):
+ W_skew = fem.integrate(
+ MFEM_RS_F.rot_penalization,
+ fields={
+ "tau": self.skew_test,
+ "sig": self.skew_trial,
+ "S": self.S,
+ "pen": self._pen_field,
+ },
+ nodal=True,
+ output=lhs._W if lhs else None,
+ output_dtype=float,
+ )
+
+ # Grad of rotated strain w.r.t R, S
+ CSk = fem.integrate(
+ self.rotated_strain_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "R": self.R, "tau": self.constraint_test},
+ output=lhs._Cs if lhs else None,
+ output_dtype=float,
+ kernel_options={"enable_backward": True},
+ )
+ CRk = fem.integrate(
+ self.incremental_strain_rotation_form,
+ nodal=True,
+ fields={
+ "sig": self.S,
+ "dR": self.skew_trial,
+ "R": self.R,
+ "tau": self.constraint_test,
+ },
+ output=lhs._Cr if lhs else None,
+ output_dtype=float,
+ kernel_options={"enable_backward": True},
+ )
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={
+ "S": self.S,
+ "sig": self.sym_trial,
+ "tau": self.sym_test,
+ "lame": self.lame_field,
+ },
+ output=lhs._H if lhs else None,
+ output_dtype=float,
+ )
+ H_pen = fem.integrate(
+ MFEM_RS_F.strain_penalization,
+ fields={
+ "tau": self.sym_test,
+ "sig": self.sym_trial,
+ "pen": self._pen_field,
+ },
+ nodal=True,
+ output=lhs._H_pen if lhs else None,
+ output_dtype=float,
+ )
+ fem.utils.array_axpy(x=H_pen.values, y=H.values)
+
+ return MFEMSystem(
+ self.A_proj, H, W_skew, self.B_proj, CSk, CRk, Bt=self.Bt_proj
+ )
+
+ def _evaluate_constraint_residual(self, out):
+ fem.integrate(
+ self.constraint_form,
+ nodal=True,
+ fields={
+ "u": self.u_field,
+ "tau": self.constraint_test,
+ "sig": self.S,
+ "R": self.R,
+ },
+ kernel_options={"enable_backward": True},
+ output=out,
+ )
+
+ if self.has_discontinuities():
+ fem.integrate(
+ self.dispgrad_side_form,
+ quadrature=self.side_quadrature,
+ fields={
+ "u": self.u_field.trace(),
+ "tau": self._constraint_side_test,
+ },
+ kernel_options={"enable_backward": True},
+ output=out,
+ add=True,
+ )
+
+ def _assemble_rhs(self, u_rhs, f, w, c_k, minus_dE_dU=None, dE_dS=None, dE_dR=None):
+ if minus_dE_dU:
+ wp.copy(src=u_rhs, dest=minus_dE_dU)
+
+ # Add current stresses
+ fem.integrate(
+ self.dispgrad_form,
+ quadrature=self.strain_quadrature,
+ fields={"u": self.u_test, "tau": self.constraint_field},
+ kernel_options={"enable_backward": True},
+ output=u_rhs,
+ add=True,
+ )
+
+ if self.has_discontinuities():
+ fem.integrate(
+ self.dispgrad_side_form,
+ quadrature=self.side_quadrature,
+ fields={
+ "u": self.u_side_test,
+ "tau": self.constraint_field.trace(),
+ },
+ kernel_options={"enable_backward": True},
+ output=u_rhs,
+ add=True,
+ )
+
+ # c_k -- constraint residual (Fk - RS)
+ self._evaluate_constraint_residual(out=c_k)
+
+ # Other primal variables:
+ # Symmetric and skew-symmetric strains
+
+ # Elastic stress + Lagrange multiplier
+ fem.integrate(
+ self.elastic_gradient_form,
+ nodal=True,
+ fields={"S": self.S, "tau": self.sym_test, "lame": self.lame_field},
+ output=f,
+ kernel_options={"enable_backward": True},
+ )
+
+ if dE_dS:
+ wp.copy(src=f, dest=dE_dS)
+
+ fem.integrate(
+ self.strain_penalization_rhs,
+ nodal=True,
+ fields={
+ "sig": self.sym_test,
+ "u": self.u_field,
+ "R": self.R,
+ "S": self.S,
+ "pen": self._pen_field,
+ },
+ output=f,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ fem.integrate(
+ self.rotated_strain_form,
+ nodal=True,
+ fields={
+ "sig": self.sym_test,
+ "R": self.R,
+ "tau": self.constraint_field,
+ },
+ output=f,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ # Rotational stress
+
+ fem.integrate(
+ self.rot_penalization_rhs,
+ nodal=True,
+ fields={
+ "sig": self.skew_test,
+ "u": self.u_field,
+ "R": self.R,
+ "S": self.S,
+ "pen": self._pen_field,
+ },
+ output=w,
+ kernel_options={"enable_backward": True},
+ )
+
+ if dE_dR:
+ dE_dR.zero_()
+
+ fem.integrate(
+ self.incremental_strain_rotation_form,
+ nodal=True,
+ fields={
+ "sig": self.S,
+ "dR": self.skew_test,
+ "R": self.R,
+ "tau": self.constraint_field,
+ },
+ output=w,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ super(MFEM_RS_F, MFEM_RS_F).add_parser_arguments(parser)
+
+ parser.add_argument("--constraint_pen", type=float, default=0.01)
+ parser.add_argument("--rot_compliance", type=float, default=0.1)
+
+ @fem.integrand
+ def constraint_form(
+ domain: Domain, s: Sample, u: Field, tau: Field, R: Field, sig: Field
+ ):
+ C = defgrad(u, s) - rotation_matrix(R(s)) * sig(s)
+ return wp.ddot(C, tau(s))
+
+ @fem.integrand
+ def dispgrad_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau^T
+ """
+ return wp.ddot(tau(s), fem.grad(u, s))
+
+ @fem.integrand
+ def dispgrad_side_form(
+ domain: Domain,
+ s: Sample,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Displacement gradient form
+ grad(u) : tau^T
+ """
+ grad_h = -wp.outer(fem.jump(u, s), fem.normal(domain, s))
+ return wp.ddot(tau(s), grad_h)
+
+ @fem.integrand
+ def rotated_strain_form(
+ s: Sample, domain: Domain, R: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R S : tau^T
+ """
+ return wp.ddot(rotation_matrix(R(s)) * sig(s), tau(s))
+
+ @fem.integrand
+ def incremental_strain_rotation_form(
+ s: Sample, domain: Domain, R: Field, dR: Field, sig: Field, tau: Field
+ ):
+ """
+ Form expressing variation of rotated deformation gradient with rotation increment
+ R dR S : tau^T
+ """
+ return wp.ddot(rotation_matrix(R(s)) * dR(s) * sig(s), tau(s))
+
+ @fem.integrand
+ def rot_penalization_rhs(
+ s: Sample,
+ sig: Field,
+ u: Field,
+ R: Field,
+ S: Field,
+ pen: Field,
+ ):
+ S_s = S(s)
+ R_s = rotation_matrix(R(s))
+ F_s = defgrad(u, s)
+ C_s = F_s - R_s * S_s
+ return -pen(s)[0] * wp.ddot(R_s * sig(s) * S_s, C_s)
+
+ @fem.integrand
+ def rot_penalization(
+ s: Sample,
+ sig: Field,
+ tau: Field,
+ S: Field,
+ pen: Field,
+ ):
+ S_s = S(s)
+ return pen(s)[0] * wp.ddot(sig(s) * S_s, tau(s) * S_s) + pen(s)[1] * wp.ddot(
+ sig(s), tau(s)
+ )
+
+ @fem.integrand
+ def strain_penalization_rhs(
+ s: Sample, sig: Field, u: Field, R: Field, S: Field, pen: Field
+ ):
+ S_s = S(s)
+ R_s = rotation_matrix(R(s))
+ F_s = defgrad(u, s)
+ C_s = F_s - R_s * S_s
+ return -pen(s)[0] * wp.ddot(R_s * sig(s), C_s)
+
+ @fem.integrand
+ def strain_penalization(
+ s: Sample,
+ sig: Field,
+ tau: Field,
+ pen: Field,
+ ):
+ return pen(s)[0] * wp.ddot(sig(s), tau(s))
+
+ @fem.integrand
+ def penalization_field(
+ s: Sample,
+ S: Field,
+ R: Field,
+ stress: Field,
+ lame: Field,
+ rot_compliance: float,
+ typ_stiff: float,
+ ):
+ rot = rotation_matrix(R(s))
+ strain = S(s)
+ sym_stress = wp.transpose(rot) * stress(s)
+
+ skew_stress = sym_stress - wp.transpose(sym_stress)
+ lbd_pen = wp.sqrt(wp.ddot(skew_stress, skew_stress)) * wp.sqrt(
+ wp.ddot(strain, strain)
+ )
+
+ return wp.vec2(typ_stiff, rot_compliance * lbd_pen)
+
+
+class MFEM_sF_S(MFEM):
+ """s(F) = S variant (Trusty SigAsia22)"""
+
+ _RHS_MODULES_LOADED = False
+ _ENERGY_MODULES_LOADED = False
+
+ def init_strain_spaces(self):
+ super().init_strain_spaces(
+ constraint_dof_mapper=fem.SymmetricTensorMapper(
+ dtype=wp.mat33, mapping=fem.SymmetricTensorMapper.Mapping.DB16
+ ),
+ )
+
+ def init_constant_forms(self):
+ super().init_constant_forms()
+
+ self.C = fem.integrate(
+ tensor_mass_form,
+ nodal=True,
+ fields={"sig": self.sym_trial, "tau": self.constraint_test},
+ output_dtype=float,
+ kernel_options={"enable_backward": True},
+ )
+
+ def _apply_rotation_delta(self, dR, alpha):
+ pass
+
+ def newton_lhs(self):
+ # Grad of rotated strain w.r.t R, S
+
+ Bk_proj = fem.integrate(
+ self.rotated_dispgrad_form,
+ fields={
+ "u_cur": self.u_field,
+ "tau": self.constraint_test,
+ "u": self.u_trial,
+ },
+ output_dtype=float,
+ quadrature=self.strain_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ sp.bsr_mm(x=Bk_proj, y=self.v_bd_matrix, z=Bk_proj, alpha=-1.0, beta=1.0)
+
+ # Elasticity -- use nodal integration so that H is block diagonal
+ H = fem.integrate(
+ self.elastic_hessian_form,
+ nodal=True,
+ fields={
+ "S": self.S,
+ "sig": self.sym_trial,
+ "tau": self.sym_test,
+ "lame": self.lame_field,
+ },
+ output_dtype=float,
+ )
+
+ return MFEMSystem(self.A_proj, H, None, Bk_proj, self.C, None)
+
+ def _evaluate_constraint_residual(self, out):
+ fem.integrate(
+ self.constraint_form,
+ nodal=True,
+ fields={
+ "u": self.u_field,
+ "tau": self.constraint_test,
+ "sig": self.S,
+ "R": self.R, # unused
+ },
+ kernel_options={"enable_backward": True},
+ output=out,
+ )
+
+ def _assemble_rhs(self, u_rhs, f, w, c_k, minus_dE_dU=None, dE_dS=None, dE_dR=None):
+ if minus_dE_dU:
+ wp.copy(src=u_rhs, dest=minus_dE_dU)
+
+ # Add current stresses
+ fem.integrate(
+ self.rotated_dispgrad_form,
+ quadrature=self.strain_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "u": self.u_test,
+ "tau": self.constraint_field,
+ },
+ kernel_options={"enable_backward": True},
+ output=u_rhs,
+ add=True,
+ )
+
+ # constraint residual
+ self._evaluate_constraint_residual(out=c_k)
+
+ # Symmetric strain
+
+ # Elastic stress + Lagrange multiplier
+ fem.integrate(
+ self.elastic_gradient_form,
+ nodal=True,
+ fields={"S": self.S, "tau": self.sym_test, "lame": self.lame_field},
+ output=f,
+ kernel_options={"enable_backward": True},
+ )
+
+ if dE_dS:
+ wp.copy(src=f, dest=dE_dS)
+
+ fem.integrate(
+ tensor_mass_form,
+ nodal=True,
+ fields={
+ "sig": self.sym_test,
+ "tau": self.constraint_field,
+ },
+ output=f,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ @fem.integrand
+ def constraint_form(
+ domain: Domain, s: Sample, u: Field, tau: Field, sig: Field, R: Field
+ ):
+ C = symmetric_strain(defgrad(u, s)) - sig(s)
+ return wp.ddot(C, tau(s))
+
+ @fem.integrand
+ def rotated_dispgrad_form(
+ domain: Domain,
+ s: Sample,
+ u_cur: Field,
+ u: Field,
+ tau: Field,
+ ):
+ """
+ Rotated deformation gradient form
+ dS : tau
+ """
+ F = defgrad(u_cur, s)
+ dF = fem.grad(u, s)
+ return wp.ddot(symmetric_strain_delta(F, dF), tau(s))
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/mfem_adaptive.py b/deps/vomp/vomp/fem/fem_examples/mfem/mfem_adaptive.py
new file mode 100644
index 0000000000000000000000000000000000000000..6191e1b11a129227ccf743b45e81c731b1c95e04
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/mfem_adaptive.py
@@ -0,0 +1,129 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+import argparse
+
+import numpy as np
+import warp as wp
+import warp.fem as fem
+import warp.examples.fem.utils as fem_example_utils
+
+from fem_examples.mfem.mfem_3d import MFEM_RS_F
+from fem_examples.mfem.softbody_sim import run_softbody_sim
+
+import polyscope as ps
+
+
+@fem.integrand
+def boundary_projector_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+):
+ """Dirichlet boundary condition projector (fixed vertices selection)"""
+
+ nor = fem.normal(domain, s)
+ pos = domain(s)
+ clamped = float(0.0)
+
+ # Single clamped point
+ # if s.qp_index < 10:
+ # clamped = 1.0
+
+ # clamped vertical sides
+ # clamped = wp.abs(nor[0])
+
+ # clamped right sides
+ clamped = wp.where(pos[0] < 1.0, 0.0, 1.0)
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@fem.integrand
+def refinement_field(s: fem.Sample, stress: fem.Field, max_p: float):
+ p = wp.abs(wp.trace(stress(s))) / max_p
+ return wp.max(0.0, 1.0 - p)
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+
+ # sim_class = ClassicFEM
+ sim_class = MFEM_RS_F
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument("--displacement", type=float, default=0.0)
+ sim_class.add_parser_arguments(parser)
+ args = parser.parse_args()
+
+ args.grid = True
+
+ vol1 = fem_example_utils.gen_volume(
+ # res=wp.vec3i(args.resolution), bounds_lo=wp.vec3(0.0, 0.5, 0.875)
+ res=wp.vec3i(args.resolution),
+ bounds_lo=wp.vec3(0.0, 0.75, 0.75),
+ )
+ coarse_grid = fem.Nanogrid(vol1)
+
+ ref_field = fem.make_polynomial_space(coarse_grid, degree=3).make_field()
+ ref_field.dof_values.fill_(1.0)
+
+ def frame_callback(displaced_pos):
+ sim.interpolate_constraint_field(strain=False)
+
+ vol_mesh = ps.get_volume_mesh("volume mesh")
+ stress = sim.interpolated_constraint_field.dof_values.numpy()
+ stress_n = np.abs(np.trace(stress, axis1=1, axis2=2))
+ max_stress = np.max(stress_n)
+ print(max_stress)
+ print(stress_n.shape)
+ vol_mesh.add_scalar_quantity("stress", stress_n, enabled=True)
+
+ for k in range(4):
+ geo = fem.adaptive_nanogrid_from_field(
+ coarse_grid=vol1, level_count=4, refinement_field=ref_field, grading="face"
+ )
+
+ sim = sim_class(geo, active_cells=None, args=args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ sim.set_boundary_condition(
+ boundary_projector_form=boundary_projector_form,
+ )
+
+ run_softbody_sim(sim, frame_callback=frame_callback)
+
+ sim.interpolate_constraint_field(strain=False)
+ stress_field = fem.NonconformingField(
+ fem.Cells(coarse_grid),
+ sim.interpolated_constraint_field,
+ background=wp.mat33f(100.0),
+ )
+ fem.interpolate(
+ refinement_field,
+ dest=ref_field,
+ fields={"stress": stress_field},
+ values={"max_p": 50.0},
+ )
+
+ # pc = ps.register_point_cloud("ref", ref_field.space.node_positions().numpy())
+ # c.add_scalar_quantity("ref", ref_field.dof_values.numpy(), enabled=True)
+ # ps.show()
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/prescribed.py b/deps/vomp/vomp/fem/fem_examples/mfem/prescribed.py
new file mode 100644
index 0000000000000000000000000000000000000000..57d53499de945b289047c7c48e05efb768b8d6d4
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/prescribed.py
@@ -0,0 +1,246 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Dict, Any
+
+import warp as wp
+import warp.fem as fem
+
+from .softbody_sim import SoftbodySim
+
+
+class QPBasedImplicitField(fem.field.GeometryField):
+ """Same as fem.ImplicitField, but passes QP index instead of grid-space position"""
+
+ def __init__(
+ self,
+ domain: fem.GeometryDomain,
+ func: wp.Function,
+ values: Optional[Dict[str, Any]] = None,
+ degree=0,
+ ):
+ self.domain = domain
+ self._degree = degree
+
+ if not isinstance(func, wp.Function):
+ raise ValueError(
+ "Implicit field function must be a warp Function (decorated with `wp.func`)"
+ )
+
+ self._func = func
+
+ argspec = fem.integrand(func.func).argspec
+ arg_types = argspec.annotations
+
+ qp_arg_type = arg_types.pop(argspec.args[0]) if arg_types else None
+ if not qp_arg_type or not wp.types.types_equal(
+ qp_arg_type,
+ int,
+ match_generic=True,
+ ):
+ raise ValueError(
+ f"QP-based Implicit field function '{func.func.__name__}' must accept an index as its first argument"
+ )
+
+ pos_arg_type = arg_types.pop(argspec.args[1]) if arg_types else None
+ if not pos_arg_type or not wp.types.types_equal(
+ pos_arg_type,
+ wp.vec3,
+ match_generic=True,
+ ):
+ raise ValueError(
+ f"QP-based Implicit field function '{func.func.__name__}' must accept a position as its second argument"
+ )
+
+ self.EvalArg = fem.cache.get_argument_struct(arg_types)
+ self.values = values
+
+ self.ElementEvalArg = self._make_element_eval_arg()
+ self.eval_degree = self._make_eval_degree()
+
+ self.eval_inner = self._make_eval_func(func)
+ self.eval_outer = self.eval_inner
+
+ @property
+ def values(self):
+ return self._func_arg
+
+ @values.setter
+ def values(self, v):
+ self._func_arg = fem.cache.populate_argument_struct(
+ self.EvalArg, v, self._func.func.__name__
+ )
+
+ @property
+ def geometry(self):
+ return self.domain.geometry
+
+ @property
+ def element_kind(self):
+ return self.domain.element_kind
+
+ def eval_arg_value(self, device):
+ return self._func_arg
+
+ @property
+ def degree(self) -> int:
+ return self._degree
+
+ @property
+ def name(self) -> str:
+ return f"Implicit_{self.domain.name}_{self.degree}_{self.EvalArg.key}"
+
+ def _make_eval_func(self, func):
+ if func is None:
+ return None
+
+ @fem.cache.dynamic_func(
+ suffix=f"{self.name}_{func.key}",
+ code_transformers=[
+ fem.cache.ExpandStarredArgumentStruct({"args.eval_arg": self.EvalArg})
+ ],
+ )
+ def eval_inner(args: self.ElementEvalArg, s: fem.Sample):
+ return func(
+ s.qp_index,
+ self.domain.element_position(args.elt_arg, s),
+ *args.eval_arg,
+ )
+
+ return eval_inner
+
+ def _make_element_eval_arg(self):
+ @fem.cache.dynamic_struct(suffix=self.name)
+ class ElementEvalArg:
+ elt_arg: self.domain.ElementArg
+ eval_arg: self.EvalArg
+
+ return ElementEvalArg
+
+ def _make_eval_degree(self):
+ ORDER = wp.constant(self._degree)
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def degree(args: self.ElementEvalArg):
+ return ORDER
+
+ return degree
+
+
+class PrescribedMotion:
+ def __init__(self, sim: SoftbodySim, quadrature: fem.PicQuadrature):
+ self.sim = sim
+ self.set_quadrature(quadrature)
+ self._prescribed_pos_field = None
+ self._prescribed_pos_weight_field = None
+
+ def set_quadrature(self, quadrature: fem.PicQuadrature):
+ self.quadrature = quadrature
+
+ def set_prescribed_positions(
+ self, pos_field: fem.field.GeometryField, weight_field: fem.field.GeometryField
+ ):
+ # for driving objects kinematically
+ self._prescribed_pos_field = pos_field
+ self._prescribed_pos_weight_field = weight_field
+
+ def add_energy(self, E: float):
+ if self._prescribed_pos_field:
+ E += fem.integrate(
+ prescribed_position_energy_form,
+ quadrature=self.quadrature,
+ fields={
+ "u_cur": self.sim.u_field,
+ "stiffness": self._prescribed_pos_weight_field,
+ "target": self._prescribed_pos_field,
+ },
+ )
+
+ return E
+
+ def add_hessian(self, lhs: wp.array):
+ if self._prescribed_pos_field:
+ z = fem.integrate(
+ prescribed_position_lhs_form,
+ quadrature=self.quadrature,
+ fields={
+ "u": self.sim.u_trial,
+ "v": self.sim.u_test,
+ "stiffness": self._prescribed_pos_weight_field,
+ },
+ output_dtype=float,
+ )
+ lhs += z
+
+ return lhs
+
+ def add_forces(self, rhs: wp.array):
+ if self._prescribed_pos_field:
+ fem.integrate(
+ prescribed_position_rhs_form,
+ quadrature=self.quadrature,
+ fields={
+ "u_cur": self.sim.u_field,
+ "v": self.sim.u_test,
+ "stiffness": self._prescribed_pos_weight_field,
+ "target": self._prescribed_pos_field,
+ },
+ output=rhs,
+ add=True,
+ )
+
+ return rhs
+
+
+@fem.integrand
+def prescribed_position_lhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+ stiffness: fem.Field,
+):
+ u_displ = u(s)
+ v_displ = v(s)
+
+ return stiffness(s) * wp.dot(u_displ, v_displ)
+
+
+@fem.integrand
+def prescribed_position_rhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ v: fem.Field,
+ stiffness: fem.Field,
+ target: fem.Field,
+):
+ pos = u_cur(s) + domain(s)
+ v_displ = v(s)
+ target_pos = target(s)
+ return stiffness(s) * wp.dot(target_pos - pos, v_displ)
+
+
+@fem.integrand
+def prescribed_position_energy_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ stiffness: fem.Field,
+ target: fem.Field,
+):
+ pos = u_cur(s) + domain(s)
+ target_pos = target(s)
+ return 0.5 * stiffness(s) * wp.length_sq(pos - target_pos)
diff --git a/deps/vomp/vomp/fem/fem_examples/mfem/softbody_sim.py b/deps/vomp/vomp/fem/fem_examples/mfem/softbody_sim.py
new file mode 100644
index 0000000000000000000000000000000000000000..61f5b72427ea567276f87ab9dcbfaf3d613f5936
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mfem/softbody_sim.py
@@ -0,0 +1,1141 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional
+import argparse
+import numpy as np
+
+import warp as wp
+import warp.sparse as sp
+import warp.fem as fem
+
+from warp.fem import Domain, Sample, Field
+from warp.fem.utils import array_axpy
+
+from warp.examples.fem.utils import bsr_cg
+
+from fem_examples.mfem.elastic_models import (
+ symmetric_strain,
+ symmetric_strain_delta,
+ snh_energy,
+ snh_stress,
+ snh_hessian_proj_analytic,
+ hooke_energy,
+ hooke_stress,
+ hooke_hessian,
+)
+from fem_examples.mfem.linalg import diff_bsr_mv
+
+wp.set_module_options({"enable_backward": False})
+wp.set_module_options({"max_unroll": 4})
+wp.set_module_options({"fast_math": True})
+
+
+@fem.integrand
+def defgrad(u: fem.Field, s: fem.Sample):
+ return fem.grad(u, s) + wp.identity(n=3, dtype=float)
+
+
+@fem.integrand
+def inertia_form(s: Sample, domain: Domain, u: Field, v: Field, rho: float, dt: float):
+ """"""
+
+ u_rhs = rho * u(s) / (dt * dt)
+ return wp.dot(u_rhs, v(s))
+
+
+@fem.integrand
+def dg_penalty_form(s: Sample, domain: Domain, u: Field, v: Field, k: float):
+ ju = fem.jump(u, s)
+ jv = fem.jump(v, s)
+
+ return wp.dot(ju, jv) * k * fem.measure_ratio(domain, s)
+
+
+@fem.integrand
+def displacement_rhs_form(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ u_prev: Field,
+ v: Field,
+ rho: float,
+ gravity: wp.vec3,
+ dt: float,
+):
+ """ + """
+ f = (
+ inertia_form(s, domain, u_prev, v, rho, dt)
+ - inertia_form(s, domain, u, v, rho, dt)
+ + rho * wp.dot(gravity, v(s))
+ )
+
+ return f
+
+
+@wp.struct
+class VolumetricForces:
+ count: int
+ centers: wp.array(dtype=wp.vec3)
+ radii: wp.array(dtype=float)
+ forces: wp.array(dtype=wp.vec3)
+ tot_weight: wp.array(dtype=float)
+
+
+@wp.func
+def force_weight(x: wp.vec3, forces: VolumetricForces, force_index: int):
+ r = wp.min(
+ wp.length(x - forces.centers[force_index])
+ / (forces.radii[force_index] + 1.0e-7),
+ 1.0,
+ )
+ r2 = r * r
+ return 2.0 * r * r2 - 3.0 * r2 + 1.0 # cubic spline
+
+
+@fem.integrand
+def force_weight_form(
+ s: Sample, domain: Domain, forces: VolumetricForces, force_index: int
+):
+ return force_weight(domain(s), forces, force_index)
+
+
+@fem.integrand
+def force_action(x: wp.vec3, forces: VolumetricForces, force_index: int, vec: wp.vec3):
+ # action of a force over a vector
+ return wp.where(
+ forces.tot_weight[force_index] >= 1.0e-6,
+ wp.dot(forces.forces[force_index], vec)
+ * force_weight(x, forces, force_index)
+ / forces.tot_weight[force_index],
+ 0.0,
+ )
+
+
+@fem.integrand
+def external_forces_form(s: Sample, domain: Domain, v: Field, forces: VolumetricForces):
+ f = float(0.0)
+ x = domain(s)
+ for fi in range(forces.count):
+ f += force_action(x, forces, fi, v(s))
+ return f
+
+
+@fem.integrand
+def kinetic_potential_energy(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+ rho: float,
+ dt: float,
+ gravity: wp.vec3,
+ forces: VolumetricForces,
+):
+ du = u(s)
+ dv = v(s)
+
+ E = rho * (0.5 * wp.dot(du - dv, du - dv) / (dt * dt) - wp.dot(du, gravity))
+ E -= external_forces_form(s, domain, u, forces)
+
+ return E
+
+
+@fem.integrand
+def geo_def_grad_field(s: Sample, domain: Domain):
+ return fem.deformation_gradient(domain, s)
+
+
+@wp.kernel(enable_backward=True)
+def scale_lame(
+ lame_out: wp.array(dtype=wp.vec2),
+ lame_ref: wp.vec2,
+ scale: wp.array(dtype=float),
+):
+ i = wp.tid()
+ lame_out[i] = lame_ref * scale[i]
+
+
+class LineSearchNaiveCriterion:
+ def __init__(self, sim):
+ self.penalty = sim.args.young_modulus
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ pass
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ f_cur = E_cur + self.penalty * C_cur
+ f_ref = E_ref + self.penalty * C_ref
+ return f_cur <= f_ref
+
+
+class LineSearchUnconstrainedArmijoCriterion:
+ def __init__(self, sim):
+ self.armijo_coeff = 0.0001
+
+ def build_linear_model(self, sim, lhs, rhs, delta_fields):
+ (delta_u,) = delta_fields
+
+ m = -wp.utils.array_inner(delta_u, sim._minus_dE_du.view(delta_u.dtype))
+ self.m = m
+
+ def accept(self, alpha, E_cur, C_cur, E_ref, C_ref):
+ return E_cur <= E_ref + self.armijo_coeff * alpha * self.m
+
+
+class SoftbodySim:
+ def __init__(self, geo: fem.Geometry, active_cells: Optional[wp.array], args):
+ self.args = args
+
+ self.geo = geo
+
+ if active_cells is None:
+ self._geo_partition = fem.geometry.WholeGeometryPartition(geo)
+ self.cells = None
+ else:
+ self._geo_partition = fem.ExplicitGeometryPartition(
+ geo, cell_mask=active_cells
+ )
+ self.cells = self._geo_partition._cells
+
+ if self.has_discontinuities() and not self.supports_discontinuities():
+ raise f"Simulator of type {type(self)} does not support discontinuities"
+
+ if not self.args.quiet:
+ print(f"Active cells: {self._geo_partition.cell_count()}")
+
+ self.dt = args.dt
+
+ self.up_axis = np.zeros(3)
+ self.up_axis[self.args.up_axis] = 1.0
+
+ self.gravity = -self.args.gravity * self.up_axis
+
+ self.forces = VolumetricForces()
+ self.forces.count = 0
+ self.forces.forces = wp.zeros(shape=(0,), dtype=wp.vec3)
+ self.forces.radii = wp.zeros(shape=(0,), dtype=float)
+ self.forces.centers = wp.zeros(shape=(0,), dtype=wp.vec3)
+ self.forces.tot_weight = wp.zeros(shape=(0,), dtype=float)
+
+ young = args.young_modulus
+ poisson = args.poisson_ratio
+ self.lame_ref = wp.vec2(
+ young / (1.0 + poisson) * np.array([poisson / (1.0 - 2.0 * poisson), 0.5])
+ )
+
+ self.typical_length = 1.0
+ self.typical_stiffness = max(
+ args.density * args.gravity * self.typical_length,
+ min(
+ args.young_modulus, # handle no-gravity, quasistatic case
+ args.density
+ * self.typical_length**2
+ / (args.dt**2), # handle no-gravity, dynamic case
+ ),
+ )
+
+ self._ls = LineSearchNaiveCriterion(self)
+ self._init_displacement_basis()
+
+ self._collision_projector_form = None
+ self._penalty_lhs_form = None
+ self._penalty_rhs_form = None
+
+ self.log = None
+
+ def has_discontinuities(self):
+ return self.args.discontinuous or (
+ hasattr(fem, "AdaptiveNanogrid")
+ and isinstance(self.geo, fem.AdaptiveNanogrid)
+ )
+
+ def supports_discontinuities(self):
+ return False
+
+ def _init_displacement_basis(self):
+ element_basis = (
+ fem.ElementBasis.SERENDIPITY
+ if self.args.serendipity
+ else fem.ElementBasis.LAGRANGE
+ )
+ self._vel_basis = fem.make_polynomial_basis_space(
+ self.geo,
+ degree=self.args.degree,
+ element_basis=element_basis,
+ discontinuous=self.args.discontinuous,
+ )
+
+ def set_displacement_basis(self, basis: fem.BasisSpace = None):
+ if basis is None:
+ self._init_displacement_basis()
+ else:
+ self._vel_basis = basis
+
+ def init_displacement_space(self):
+ args = self.args
+
+ u_space = fem.make_collocated_function_space(self._vel_basis, dtype=wp.vec3)
+ u_space_partition = fem.make_space_partition(
+ space_topology=self._vel_basis.topology,
+ geometry_partition=self._geo_partition,
+ with_halo=False,
+ )
+
+ # Defines some fields over our function spaces
+ self.u_field = u_space.make_field(
+ space_partition=u_space_partition
+ ) # displacement
+ self.du_field = u_space.make_field(
+ space_partition=u_space_partition
+ ) # displacement delta
+ self.du_prev = u_space.make_field(
+ space_partition=u_space_partition
+ ) # displacement delta
+ self.force_field = u_space.make_field(
+ space_partition=u_space_partition
+ ) # total force field -- for collision filtering
+
+ # Since our spaces are constant, we can also predefine the test/trial functions that we will need for integration
+ self.u_trial = fem.make_trial(space=u_space, space_partition=u_space_partition)
+ self.u_test = fem.make_test(space=u_space, space_partition=u_space_partition)
+
+ self.vel_quadrature = fem.RegularQuadrature(
+ self.u_test.domain, order=2 * args.degree
+ )
+
+ if self.has_discontinuities():
+ sides = fem.Sides(self.geo)
+ self.u_side_trial = fem.make_trial(
+ space=u_space, space_partition=u_space_partition, domain=sides
+ )
+ self.u_side_test = fem.make_test(
+ space=u_space, space_partition=u_space_partition, domain=sides
+ )
+
+ self.side_quadrature = fem.RegularQuadrature(
+ self.u_side_test.domain, order=2 * args.degree
+ )
+
+ # Create material parameters space with same basis as deformation field
+ lame_space = fem.make_polynomial_space(self.geo, dtype=wp.vec2, degree=0)
+
+ self.lame_field = lame_space.make_field()
+ self.lame_field.dof_values.fill_(self.lame_ref)
+
+ # For interpolating the stress/strain fields back to velocity space
+ interpolated_constraint_space = fem.make_collocated_function_space(
+ self._vel_basis, dtype=wp.mat33
+ )
+ self.interpolated_constraint_field = interpolated_constraint_space.make_field(
+ space_partition=u_space_partition
+ )
+
+ self._mass_space = fem.make_collocated_function_space(
+ self._vel_basis, dtype=float
+ )
+ self._mass = None
+
+ def set_boundary_condition(
+ self,
+ boundary_projector_form,
+ boundary_displacement_form=None,
+ boundary_displacement_args={},
+ ):
+ u_space = self.u_field.space
+
+ # Displacement boundary conditions
+ boundary = fem.BoundarySides(self._geo_partition)
+
+ u_bd_test = fem.make_test(
+ space=u_space,
+ space_partition=self.u_test.space_partition,
+ domain=boundary,
+ )
+ u_bd_trial = fem.make_trial(
+ space=u_space,
+ space_partition=self.u_test.space_partition,
+ domain=boundary,
+ )
+ self.v_bd_rhs = None
+ if boundary_displacement_form is not None:
+ self.v_bd_rhs = fem.integrate(
+ boundary_displacement_form,
+ fields={"v": u_bd_test},
+ values=boundary_displacement_args,
+ nodal=True,
+ output_dtype=wp.vec3f,
+ )
+ if boundary_projector_form is not None:
+ self.v_bd_matrix = fem.integrate(
+ boundary_projector_form,
+ fields={"u": u_bd_trial, "v": u_bd_test},
+ nodal=True,
+ output_dtype=float,
+ )
+ else:
+ self.v_bd_matrix = sp.bsr_zeros(
+ self.u_trial.space_partition.node_count(),
+ self.u_trial.space_partition.node_count(),
+ block_type=wp.mat33,
+ )
+ fem.normalize_dirichlet_projector(self.v_bd_matrix, self.v_bd_rhs)
+
+ def set_fixed_points_condition(
+ self,
+ fixed_points_projector_form,
+ fixed_point_projector_args={},
+ ):
+ self.v_bd_rhs = None
+ self.v_bd_matrix = fem.integrate(
+ fixed_points_projector_form,
+ fields={"u": self.u_trial, "v": self.u_test, "u_cur": self.u_field},
+ values=fixed_point_projector_args,
+ nodal=True,
+ output_dtype=float,
+ )
+
+ self.v_bd_rhs = None
+ fem.normalize_dirichlet_projector(self.v_bd_matrix, self.v_bd_rhs)
+
+ def set_fixed_points_displacement(
+ self,
+ fixed_points_displacement_field=None,
+ fixed_points_displacement_args={},
+ ):
+ bd_field = self.u_test.space.make_field(
+ space_partition=self.u_test.space_partition
+ )
+ fem.interpolate(
+ fixed_points_displacement_field,
+ dest=bd_field,
+ fields={"u_cur": self.u_field},
+ values=fixed_points_displacement_args,
+ )
+
+ self.v_bd_rhs = bd_field.dof_values
+
+ def init_constant_forms(self):
+ args = self.args
+
+ if self.args.lumped_mass:
+ self.A = fem.integrate(
+ inertia_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt},
+ output_dtype=float,
+ nodal=True,
+ )
+ else:
+ self.A = fem.integrate(
+ inertia_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ values={"rho": args.density, "dt": self.dt},
+ output_dtype=float,
+ quadrature=self.vel_quadrature,
+ )
+
+ if self.has_discontinuities():
+ self.A += fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_side_trial, "v": self.u_side_test},
+ values={"k": self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output_dtype=float,
+ )
+
+ if self._penalty_lhs_form:
+ self.A += fem.integrate(
+ self._penalty_lhs_form,
+ fields={"u": self.u_trial, "v": self.u_test},
+ output_dtype=float,
+ quadrature=self.vel_quadrature,
+ )
+
+ self.A.nnz_sync()
+
+ self.update_force_weight()
+
+ def project_constant_forms(self):
+ self.A_proj = sp.bsr_copy(self.A)
+ fem.dirichlet.project_system_matrix(self.A_proj, self.v_bd_matrix)
+ self.A_proj.nnz_sync()
+
+ def update_force_weight(self):
+ self.forces.tot_weight = wp.empty(shape=self.forces.count, dtype=wp.float32)
+ for fi in range(self.forces.count):
+ wi = self.forces.tot_weight[fi : fi + 1]
+ fem.integrate(
+ force_weight_form,
+ quadrature=self.vel_quadrature,
+ values={
+ "force_index": fi,
+ "forces": self.forces,
+ },
+ output=wi,
+ accumulate_dtype=wp.float32,
+ )
+
+ def constraint_free_rhs(self, dt=None, with_external_forces=True, tape=None):
+ args = self.args
+
+ gravity = self.gravity if with_external_forces else wp.vec3(0.0)
+
+ # Quasi-quasistatic: normal dt in lhs (trust region), large dt in rhs (quasistatic)
+
+ with_gradient = tape is not None
+ rhs_tape = wp.Tape() if tape is None else tape
+ rhs = wp.zeros(
+ dtype=wp.vec3,
+ requires_grad=with_gradient,
+ shape=self.u_test.space_partition.node_count(),
+ )
+
+ with rhs_tape:
+ fem.integrate(
+ displacement_rhs_form,
+ fields={"u": self.du_field, "u_prev": self.du_prev, "v": self.u_test},
+ values={"rho": args.density, "dt": self._step_dt(), "gravity": gravity},
+ output=rhs,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self.has_discontinuities():
+ fem.integrate(
+ dg_penalty_form,
+ fields={"u": self.u_field.trace(), "v": self.u_side_test},
+ values={"k": -self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output=rhs,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ if self._penalty_rhs_form:
+ fem.integrate(
+ self._penalty_rhs_form,
+ fields={"u": self.u_field, "v": self.u_test},
+ output=rhs,
+ add=True,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": True},
+ )
+
+ if with_external_forces and self.forces.count > 0:
+ # NOT differentiating with respect to external forces
+ # Those are assumed to not depend on the geometry
+ fem.integrate(
+ external_forces_form,
+ fields={"v": self.u_test},
+ values={
+ "forces": self.forces,
+ },
+ output_dtype=wp.vec3,
+ quadrature=self.vel_quadrature,
+ kernel_options={"enable_backward": False},
+ output=rhs,
+ add=True,
+ )
+
+ return rhs
+
+ def run_frame(self):
+ (self.du_field, self.du_prev) = (self.du_prev, self.du_field)
+
+ if self.args.quasi_quasistatic:
+ self.du_prev.dof_values.zero_()
+
+ self.compute_initial_guess()
+
+ tol = self.args.newton_tol**2
+
+ E_cur, C_cur = self.evaluate_energy()
+ cumulative_time = 0.0
+
+ if not self.args.quiet:
+ print(f"Newton initial guess: E={E_cur}, Cr={C_cur}")
+ if self.log:
+ mean_displ = np.mean(
+ np.linalg.norm(self.du_field.dof_values.numpy(), axis=1)
+ )
+ print(
+ "\t".join(
+ str(x)
+ for x in (0, E_cur, C_cur, mean_displ, 0.0, 0.0, cumulative_time)
+ ),
+ file=self.log,
+ )
+
+ for k in range(self.args.n_newton):
+ with wp.ScopedTimer(f"Iter {k}", print=False) as timer:
+ E_ref, C_ref = E_cur, C_cur
+ self.checkpoint_newton_values()
+
+ self.prepare_newton_step()
+ rhs = self.newton_rhs()
+ lhs = self.newton_lhs()
+ delta_fields = self.solve_newton_system(lhs, rhs)
+
+ self.apply_newton_deltas(delta_fields)
+ E_cur, C_cur = self.evaluate_energy()
+
+ ddu = delta_fields[0]
+ step_size = wp.utils.array_inner(ddu, ddu) / (1 + ddu.shape[0])
+
+ # linear model
+ self._ls.build_linear_model(self, lhs, rhs, delta_fields)
+
+ # Line search
+ alpha = 1.0
+ for j in range(self.args.n_backtrack):
+ if self._ls.accept(alpha, E_cur, C_cur, E_ref, C_ref):
+ break
+
+ alpha = 0.5 * alpha
+ self.apply_newton_deltas(delta_fields, alpha=alpha)
+ E_cur, C_cur = self.evaluate_energy()
+
+ if not self.args.quiet:
+ print(
+ f"Newton iter {k}: E={E_cur}, Cr={C_cur}, step size {np.sqrt(step_size)}, alpha={alpha}"
+ )
+
+ cumulative_time += timer.elapsed
+ if self.log:
+ mean_displ = np.mean(
+ np.linalg.norm(self.du_field.dof_values.numpy(), axis=1)
+ )
+ print(
+ "\t".join(
+ str(x)
+ for x in (
+ k + 1,
+ E_cur,
+ C_cur,
+ mean_displ,
+ step_size,
+ alpha,
+ cumulative_time,
+ )
+ ),
+ file=self.log,
+ )
+
+ if step_size < tol:
+ break
+
+ def prepare_newton_step(self, tape=None):
+ pass
+
+ def checkpoint_newton_values(self):
+ self._u_cur = wp.clone(self.u_field.dof_values)
+ self._du_cur = wp.clone(self.du_field.dof_values)
+
+ def apply_newton_deltas(self, delta_fields, alpha=1.0):
+ # Restore checkpoint
+ wp.copy(src=self._u_cur, dest=self.u_field.dof_values)
+ wp.copy(src=self._du_cur, dest=self.du_field.dof_values)
+
+ # Add to total displacement
+ if alpha == 0.0:
+ return
+
+ delta_du = delta_fields[0]
+ array_axpy(x=delta_du, y=self.u_field.dof_values, alpha=alpha)
+ array_axpy(x=delta_du, y=self.du_field.dof_values, alpha=alpha)
+
+ def _step_dt(self):
+ # In fake quasistatic mode, use a large timestep for the rhs computation
+ # Note that self.dt is still use to compute lhs (inertia matrix)
+ return 1.0e6 if self.args.quasi_quasistatic else self.dt
+
+ def evaluate_energy(self, E_u=None, cr=None):
+ E_u = fem.integrate(
+ kinetic_potential_energy,
+ quadrature=self.vel_quadrature,
+ fields={"u": self.du_field, "v": self.du_prev},
+ values={
+ "rho": self.args.density,
+ "dt": self._step_dt(),
+ "gravity": self.gravity,
+ "forces": self.forces,
+ },
+ output=E_u,
+ )
+
+ if self.has_discontinuities():
+ if wp.types.is_array(E_u):
+ Eu_dg = wp.empty_like(E_u)
+ else:
+ Eu_dg = None
+
+ Eu_dg = fem.integrate(
+ dg_penalty_form,
+ domain=fem.Sides(self.geo),
+ fields={"u": self.u_field.trace(), "v": self.u_field.trace()},
+ values={"k": self.typical_stiffness * self.args.dg_jump_pen},
+ quadrature=self.side_quadrature,
+ output=Eu_dg,
+ )
+
+ if wp.types.is_array(E_u):
+ fem.utils.array_axpy(y=E_u, x=Eu_dg)
+ else:
+ E_u += Eu_dg
+
+ return E_u, 0.0
+
+ def _filter_forces(self, u_rhs, tape):
+ if self._collision_projector_form is not None:
+ # update collision projector, if required
+ self.force_field.dof_values = u_rhs
+
+ self.v_bd_matrix = fem.integrate(
+ self._collision_projector_form,
+ fields={
+ "u": self.u_trial,
+ "v": self.u_test,
+ "u_cur": self.u_field,
+ "f": self.force_field,
+ },
+ values=self._collision_projector_args,
+ nodal=True,
+ output_dtype=float,
+ )
+ fem.normalize_dirichlet_projector(self.v_bd_matrix)
+ self.project_constant_forms()
+
+ proj_tape = wp.Tape() if tape is None else tape
+ with proj_tape:
+ diff_bsr_mv(
+ A=self.v_bd_matrix,
+ x=u_rhs,
+ y=u_rhs,
+ alpha=-1.0,
+ beta=1.0,
+ self_adjoint=True,
+ )
+
+ def compute_initial_guess(self):
+ # Self-advect
+ self.du_field.dof_values.zero_()
+ rhs = self.constraint_free_rhs(with_external_forces=False)
+
+ fem.dirichlet.project_system_rhs(self.A, rhs, self.v_bd_matrix, self.v_bd_rhs)
+
+ bsr_cg(self.A_proj, b=rhs, x=self.du_field.dof_values, quiet=True)
+
+ array_axpy(x=self.du_field.dof_values, y=self.u_field.dof_values)
+
+ def scale_lame_field(self, stiffness_scale_array: wp.array):
+ # make a copy of the original field for differentiability
+ wp.launch(
+ scale_lame,
+ dim=self.lame_field.dof_values.shape[0],
+ inputs=[
+ self.lame_field.dof_values,
+ self.lame_ref,
+ stiffness_scale_array,
+ ],
+ )
+
+ def reset_fields(self):
+ self.u_field.dof_values.zero_()
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ parser.add_argument("--degree", type=int, default=1)
+ parser.add_argument("--serendipity", action="store_true", default=False)
+ parser.add_argument("-n", "--n_frames", type=int, default=-1)
+ parser.add_argument("--n_newton", type=int, default=2)
+ parser.add_argument("--newton_tol", type=float, default=1.0e-4)
+ parser.add_argument("--cg_tol", type=float, default=1.0e-8)
+ parser.add_argument("--cg_iters", type=float, default=1000)
+ parser.add_argument("--n_backtrack", type=int, default=4)
+ parser.add_argument("--young_modulus", type=float, default=250.0)
+ parser.add_argument("--poisson_ratio", type=float, default=0.4)
+ parser.add_argument("--gravity", type=float, default=10.0)
+ parser.add_argument("--up_axis", "-up", type=int, default=1)
+ parser.add_argument("--density", type=float, default=1.0)
+ parser.add_argument("--dt", type=float, default=0.1)
+ parser.add_argument(
+ "--quasi_quasistatic", "-qqs", action=argparse.BooleanOptionalAction
+ )
+ parser.add_argument(
+ "-nh", "--neo_hookean", action=argparse.BooleanOptionalAction
+ )
+ parser.add_argument(
+ "-dg", "--discontinuous", action=argparse.BooleanOptionalAction
+ )
+ parser.add_argument("--dg_jump_pen", type=float, default=1.0)
+ parser.add_argument("-opt", "--optimize", action="store_true", default=False)
+ parser.add_argument("-ss", "--step_size", type=float, default=0.001)
+ parser.add_argument("--quiet", action="store_true", default=False)
+ parser.add_argument("--lumped_mass", action=argparse.BooleanOptionalAction)
+ parser.add_argument("--fp64", action="store_true", default=False)
+
+
+class ClassicFEM(SoftbodySim):
+ def __init__(self, geo: fem.Geometry, active_cells: wp.array, args):
+ super().__init__(geo, active_cells, args)
+
+ self._ls = LineSearchUnconstrainedArmijoCriterion(self)
+
+ self._make_elasticity_forms()
+
+ def _make_elasticity_forms(self):
+ if self.args.neo_hookean:
+ self.elastic_energy = ClassicFEM.nh_elastic_energy
+ self.elastic_forces = ClassicFEM.nh_elastic_forces
+ self.elasticity_hessian = ClassicFEM.nh_elasticity_hessian
+ self.stress_field = ClassicFEM.nh_stress_field
+ else:
+ self.elastic_energy = ClassicFEM.cr_elastic_energy
+ self.elastic_forces = ClassicFEM.cr_elastic_forces
+ self.elasticity_hessian = ClassicFEM.cr_elasticity_hessian
+ self.stress_field = ClassicFEM.cr_stress_field
+
+ def evaluate_energy(self):
+ E_u, c_r = super().evaluate_energy()
+
+ E_e = fem.integrate(
+ self.elastic_energy,
+ quadrature=self.elasticity_quadrature,
+ fields={"u_cur": self.u_field, "lame": self.lame_field},
+ )
+
+ E_tot = E_u + E_e
+
+ return E_tot, c_r
+
+ def init_strain_spaces(self):
+ self.elasticity_quadrature = self.vel_quadrature
+ self.constraint_field = self.interpolated_constraint_field
+ self._constraint_field_restriction = fem.make_restriction(
+ self.constraint_field, space_restriction=self.u_test.space_restriction
+ )
+
+ def set_strain_basis(self, strain_basis: fem.BasisSpace):
+ pass
+
+ def compute_initial_guess(self):
+ # Start from last frame pose, known good state
+ self.du_field.dof_values.zero_()
+
+ def newton_lhs(self):
+ u_matrix = fem.integrate(
+ self.elasticity_hessian,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "u": self.u_trial,
+ "v": self.u_test,
+ "lame": self.lame_field,
+ },
+ output_dtype=float,
+ )
+
+ u_matrix += self.A
+ fem.dirichlet.project_system_matrix(u_matrix, self.v_bd_matrix)
+
+ return u_matrix
+
+ def newton_rhs(self, tape: wp.Tape = None):
+ u_rhs = self.constraint_free_rhs(tape=tape)
+
+ rhs_tape = wp.Tape() if tape is None else tape
+ with rhs_tape:
+ fem.integrate(
+ self.elastic_forces,
+ quadrature=self.elasticity_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "v": self.u_test,
+ "lame": self.lame_field,
+ },
+ output=u_rhs,
+ add=True,
+ kernel_options={"enable_backward": True},
+ )
+
+ self._minus_dE_du = wp.clone(u_rhs, requires_grad=False)
+
+ self._filter_forces(u_rhs, tape=tape)
+
+ return u_rhs
+
+ @staticmethod
+ def _solve_fp64(lhs, rhs, res, maxiters, tol=None):
+ lhs64 = sp.bsr_copy(lhs, scalar_type=wp.float64)
+ rhs64 = wp.empty(shape=rhs.shape, dtype=wp.vec3d, device=rhs.device)
+ wp.utils.array_cast(in_array=rhs, out_array=rhs64)
+
+ res64 = wp.zeros_like(rhs64)
+ bsr_cg(
+ A=lhs64,
+ b=rhs64,
+ x=res64,
+ quiet=True,
+ tol=tol,
+ max_iters=maxiters,
+ )
+
+ wp.utils.array_cast(in_array=res64, out_array=res)
+
+ return res
+
+ def solve_newton_system(self, lhs, rhs):
+ if self.args.fp64:
+ res = wp.empty_like(rhs)
+ ClassicFEM._solve_fp64(
+ lhs, rhs, res, maxiters=self.args.cg_iters, tol=self.args.cg_tol
+ )
+ return (res,)
+
+ res = wp.zeros_like(rhs)
+ bsr_cg(
+ A=lhs,
+ b=rhs,
+ x=res,
+ quiet=True,
+ tol=self.args.cg_tol,
+ max_iters=self.args.cg_iters,
+ )
+ return (res,)
+
+ def record_adjoint(self, tape):
+ # The forward Newton is finding a root of rhs(q, p) = 0 with q = (u, S, R, lambda)
+ # so drhs/dp = drhs/dq dq/dp + drhs/dp = 0
+ # [- drhs/dq] dq/dp = drhs/dp
+ # lhs dq/dp = drhs/dp
+
+ self.prepare_newton_step(tape=tape)
+ rhs = self.newton_rhs(tape=tape)
+ lhs = self.newton_lhs()
+
+ def solve_backward():
+ adj_res = self.u_field.dof_values.grad
+ ClassicFEM._solve_fp64(lhs, adj_res, rhs.grad, maxiters=self.args.cg_iters)
+
+ tape.record_func(
+ solve_backward,
+ arrays=[
+ self.u_field.dof_values,
+ rhs,
+ ],
+ )
+
+ # So we can compute stress-based losses
+ with tape:
+ self.interpolate_constraint_field()
+
+ def interpolate_constraint_field(self, strain=False):
+ field = self.strain_field if strain else self.stress_field
+
+ fem.interpolate(
+ field,
+ fields={
+ "u_cur": self.u_field,
+ "lame": self.lame_field,
+ },
+ kernel_options={"enable_backward": True},
+ dest=self._constraint_field_restriction,
+ )
+
+ @fem.integrand
+ def nh_elastic_energy(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ return snh_energy(F, lame(s))
+
+ @fem.integrand
+ def nh_elastic_forces(s: Sample, u_cur: Field, v: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ tau = fem.grad(v, s)
+
+ return -wp.ddot(tau, snh_stress(F, lame(s)))
+
+ @fem.integrand
+ def nh_stress_field(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ return snh_stress(F, lame(s))
+
+ @fem.integrand
+ def nh_elasticity_hessian(s: Sample, u_cur: Field, u: Field, v: Field, lame: Field):
+ F_s = defgrad(u_cur, s)
+ tau_s = fem.grad(v, s)
+ sig_s = fem.grad(u, s)
+ lame_s = lame(s)
+
+ return snh_hessian_proj_analytic(F_s, tau_s, sig_s, lame_s)
+
+ @fem.integrand
+ def cr_elastic_energy(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ S = symmetric_strain(F)
+ return hooke_energy(S, lame(s))
+
+ @fem.integrand
+ def cr_elastic_forces(s: Sample, u_cur: Field, v: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ S = symmetric_strain(F)
+ tau = symmetric_strain_delta(F, fem.grad(v, s))
+ return -wp.ddot(tau, hooke_stress(S, lame(s)))
+
+ @fem.integrand
+ def cr_stress_field(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ S = symmetric_strain(F)
+ return hooke_stress(S, lame(s))
+
+ @fem.integrand
+ def cr_elasticity_hessian(s: Sample, u_cur: Field, u: Field, v: Field, lame: Field):
+ F_s = defgrad(u_cur, s)
+ S_s = symmetric_strain(F_s)
+ tau_s = symmetric_strain_delta(F_s, fem.grad(v, s))
+ sig_s = symmetric_strain_delta(F_s, fem.grad(u, s))
+ lame_s = lame(s)
+
+ return hooke_hessian(S_s, tau_s, sig_s, lame_s)
+
+ @fem.integrand
+ def strain_field(s: Sample, u_cur: Field, lame: Field):
+ F = defgrad(u_cur, s)
+ return symmetric_strain(F)
+
+
+def run_softbody_sim(
+ sim: SoftbodySim,
+ init_callback=None,
+ frame_callback=None,
+ ui=True,
+ log=None,
+ shutdown=False,
+):
+ if log is not None:
+ with open(log, "w") as log_f:
+ sim.log = log_f
+ run_softbody_sim(sim, init_callback, frame_callback, ui=ui, log=None)
+ sim.log = None
+ return
+
+ if not ui:
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ sim.cur_frame = 0
+ if init_callback:
+ init_callback()
+
+ active_indices = sim.u_field.space_partition.space_node_indices().numpy()
+
+ for frame in range(sim.args.n_frames):
+ sim.cur_frame = frame + 1
+ with wp.ScopedTimer(f"--- Frame --- {sim.cur_frame}", synchronize=True):
+ sim.run_frame()
+
+ displaced_pos = sim.u_field.space.node_positions().numpy()
+ displaced_pos[active_indices] += sim.u_field.dof_values.numpy()
+
+ if frame_callback:
+ frame_callback(displaced_pos)
+ return
+
+ import polyscope as ps
+
+ active_cells = None if sim.cells is None else sim.cells.array.numpy()
+
+ try:
+ hexes = sim.u_field.space.node_hexes()
+
+ if active_cells is not None:
+ hex_per_cell = len(hexes) // sim.geo.cell_count()
+ selected_hexes = np.broadcast_to(
+ (active_cells * hex_per_cell).reshape(len(active_cells), 1),
+ shape=(len(active_cells), hex_per_cell),
+ )
+ selected_hexes = selected_hexes + np.broadcast_to(
+ np.arange(hex_per_cell).reshape(1, hex_per_cell),
+ shape=(len(active_cells), hex_per_cell),
+ )
+
+ hexes = hexes[selected_hexes.flatten()]
+
+ except AttributeError:
+ hexes = None
+
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+
+ if active_cells is not None:
+ tet_per_cell = len(tets) // sim.geo.cell_count()
+ selected_tets = np.broadcast_to(
+ (active_cells * tet_per_cell).reshape(len(active_cells), 1),
+ shape=(len(active_cells), tet_per_cell),
+ )
+ selected_tets = selected_tets + np.broadcast_to(
+ np.arange(tet_per_cell).reshape(1, tet_per_cell),
+ shape=(len(active_cells), tet_per_cell),
+ )
+
+ tets = tets[selected_tets.flatten()]
+
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ ps.init()
+ ps.set_ground_plane_mode(mode_str="none")
+ ps.set_ground_plane_height(0.0)
+
+ node_pos = sim.u_field.space.node_positions().numpy()
+
+ ps_vol = ps.register_volume_mesh(
+ "volume mesh", node_pos, hexes=hexes, tets=tets, edge_width=1.0
+ )
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+ sim.cur_frame = 0
+
+ if init_callback:
+ init_callback()
+
+ active_indices = sim.u_field.space_partition.space_node_indices().numpy()
+
+ def callback():
+ sim.cur_frame = sim.cur_frame + 1
+ if sim.args.n_frames >= 0 and sim.cur_frame > sim.args.n_frames:
+ if shutdown:
+ ps.unshow()
+ return
+
+ with wp.ScopedTimer(f"--- Frame --- {sim.cur_frame}", synchronize=True):
+ sim.run_frame()
+
+ displaced_pos = sim.u_field.space.node_positions().numpy()
+ displaced_pos[active_indices] += sim.u_field.dof_values.numpy()
+ ps_vol.update_vertex_positions(displaced_pos)
+
+ if frame_callback:
+ frame_callback(displaced_pos)
+
+ # ps.screenshot()
+
+ ps.set_user_callback(callback)
+ # ps.look_at(target=(0.5, 0.5, 0.5), camera_location=(0.5, 0.5, 2.5))
+ ps.show()
diff --git a/deps/vomp/vomp/fem/fem_examples/model_problems/elastic_wave.py b/deps/vomp/vomp/fem/fem_examples/model_problems/elastic_wave.py
new file mode 100644
index 0000000000000000000000000000000000000000..64719981f0e73cd6b9fd537c9855b3686499d318
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/model_problems/elastic_wave.py
@@ -0,0 +1,285 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This example solves an elastic wave propagation problem for a displacement field u:
+
+rho d2 u / dt2 - Div[ sigma + phi(t) Id ] = 0 # conservation of momentum
+sigma = mu * eps + lambda * Trace(eps) * Id # stress-strain relationship
+eps = D(u) := 1/2 (grad u + (grad u)^T) # strain tensor
+with u.n = 0 on the domain boundary
+
+with phi(t) a time-varying source term
+
+
+After integrating against test functions and using integration by part for the stress terms,
+we can express a backwards Euler integrator as follow:
+
+m(du^k, v) + k(du^k, v) = m(du^{k-1}, v) - k(u^{k+1}, v) + f(t, v) for all v
+du^k.n = 0 on the domain boundary
+u^k = u^{k+1} + du^K
+
+with:
+m(u, v) = int{ u.v / dt^2 }
+k(u, v) = int{ sigma(D(u)) : D(v) } with sigma(eps) the strain-stress relationship
+f(t, v) = int{ phi(t) div(v) } the source term
+
+This yields a linear system (M + K) du = rhs, which is then projected to satisfy boudary conditions
+and solved with conjugate gradient
+
+"""
+
+import argparse
+
+import warp as wp
+import warp.fem as fem
+import warp.examples.fem.utils as fem_example_utils
+
+
+import meshio
+
+
+# Corresponds to sigma(eps) strain-stress relationship
+@wp.func
+def strain_stress_relationship(strain: wp.mat22, lame_lambda: float, lame_mu: float):
+ return 2.0 * lame_mu * strain + lame_lambda * wp.trace(strain) * wp.identity(
+ n=2, dtype=float
+ )
+
+
+# Corresponds to k(u, v) bilinear form
+@fem.integrand
+def stress_form(
+ s: fem.Sample, u: fem.Field, v: fem.Field, lame_lambda: float, lame_mu: float
+):
+ strain = fem.D(u, s)
+ stress = strain_stress_relationship(strain, lame_lambda, lame_mu)
+ return wp.ddot(fem.D(v, s), stress)
+
+
+# Corresponds to m(u, v) bilinear form
+@fem.integrand
+def inertia_form(s: fem.Sample, u: fem.Field, v: fem.Field, rho: float, dt: float):
+ return wp.dot(u(s), v(s)) * rho / (dt * dt)
+
+
+# Corresponds to phi(t) source term
+@wp.func
+def source_term(x: wp.vec2, t: float, dt: float, dx: float):
+ src = wp.sin(t / (20.0 * dt))
+
+ r_sq = wp.length_sq(x) / (4.0 * dx * dx)
+ return src * wp.max(0.0, 1.0 - r_sq)
+
+
+# Sum of all rhs terms
+@fem.integrand
+def rhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ du: fem.Field,
+ v: fem.Field,
+ rho: float,
+ t: float,
+ dt: float,
+ dx: float,
+ x0: wp.vec2,
+ lame_lambda: float,
+ lame_mu: float,
+):
+ return (
+ inertia_form(s, du, v, rho, dt)
+ - stress_form(s, u, v, lame_lambda, lame_mu)
+ + lame_lambda * source_term(domain(s) - x0, t, dt, dx) * fem.div(v, s)
+ )
+
+
+# Boundary normal field, for free-slip boundary condition.
+# Used to average the discontinuous per-side normals at nodes
+@fem.integrand
+def normal_field(s: fem.Sample, domain: fem.Domain):
+ return fem.normal(domain, s)
+
+
+# Free-slip (reflecting) boundary condition
+@fem.integrand
+def boundary_projector_form(
+ s: fem.Sample, u: fem.Field, v: fem.Field, normal: fem.Field
+):
+ n = normal(s)
+ return wp.dot(u(s), n) * wp.dot(v(s), n)
+
+
+class Example:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("mesh")
+ parser.add_argument("--resolution", type=int, default=25)
+ parser.add_argument("--degree", type=int, default=1)
+ parser.add_argument("-n", "--n_frames", type=int, default=250)
+ parser.add_argument("--density", type=float, default=1000.0)
+ parser.add_argument("--young_modulus", type=float, default=100.0)
+ parser.add_argument("--poisson_ratio", type=float, default=0.95)
+ parser.add_argument("--dt", type=float, default=0.01)
+
+ def __init__(self, stage=None, quiet=False, args=None, **kwargs):
+ if args is None:
+ # Read args from kwargs, add default arg values from parser
+ args = argparse.Namespace(**kwargs)
+ args = Example.parser.parse_args(args=[], namespace=args)
+ self._args = args
+ self._quiet = quiet
+
+ self.current_time = 0.0
+
+ # Read mesh and create FEM geometry
+ mesh: meshio.Mesh = meshio.read(args.mesh)
+ positions = wp.array(mesh.points[:, :2], dtype=wp.vec2)
+ tri_vidx = wp.array(mesh.cells_dict["triangle"], dtype=int)
+ self._geo = fem.Trimesh2D(tri_vertex_indices=tri_vidx, positions=positions)
+
+ # Convert Young/Poisson to Lame coefficients
+ young = args.young_modulus
+ poisson = args.poisson_ratio
+ self.lame_lambda = young / (1.0 + poisson) * poisson / (1.0 - poisson)
+ self.lame_mu = 0.5 * young / (1.0 + poisson)
+
+ # Displacement function space
+ u_space = fem.make_polynomial_space(
+ self._geo,
+ degree=args.degree,
+ dtype=wp.vec2,
+ element_basis=fem.ElementBasis.LAGRANGE,
+ )
+
+ # Displacement and displacement delta (i.e. velocity * dt) fields
+ self.u_field = u_space.make_field()
+ self._du_field = u_space.make_field()
+
+ # Assemble constant matrices
+
+ # Inertia and elasticity terms
+ domain = fem.Cells(geometry=self._geo)
+ u_test = fem.make_test(space=u_space, domain=domain)
+ u_trial = fem.make_trial(space=u_space, domain=domain)
+
+ inertia_matrix = fem.integrate(
+ inertia_form,
+ fields={"u": u_trial, "v": u_test},
+ values={
+ "dt": args.dt,
+ "rho": self._args.density,
+ },
+ )
+ elasticity_matrix = fem.integrate(
+ stress_form,
+ fields={"u": u_trial, "v": u_test},
+ values={"lame_lambda": self.lame_lambda, "lame_mu": self.lame_mu},
+ )
+
+ self._system_matrix = elasticity_matrix + inertia_matrix
+
+ # Free-slip boundary conditions: zero in normal direction
+ boundary = fem.BoundarySides(self._geo)
+
+ # First we need a continuous normal field; interpolate per-side normals to a continuous field
+ vertex_normal_field = u_space.make_field()
+ fem.interpolate(
+ normal_field,
+ dest=fem.make_restriction(vertex_normal_field, domain=boundary),
+ )
+
+ # Now build our projector
+ u_bd_test = fem.make_test(space=u_space, domain=boundary)
+ u_bd_trial = fem.make_trial(space=u_space, domain=boundary)
+ u_bd_projector = fem.integrate(
+ boundary_projector_form,
+ fields={
+ "u": u_bd_trial,
+ "v": u_bd_test,
+ "normal": vertex_normal_field.trace(),
+ },
+ nodal=True,
+ )
+ fem.dirichlet.normalize_dirichlet_projector(u_bd_projector)
+ self._boundary_projector = u_bd_projector
+
+ self._projected_system_matrix = wp.sparse.bsr_copy(self._system_matrix)
+ fem.dirichlet.project_system_matrix(
+ projector_matrix=self._boundary_projector,
+ system_matrix=self._projected_system_matrix,
+ )
+
+ # Save test function, we'll reuse for rhs
+ self._u_test = u_test
+
+ def update(self):
+ # Assemble time-varying righ-hand-side
+ u_rhs = fem.integrate(
+ rhs_form,
+ fields={"du": self._du_field, "u": self.u_field, "v": self._u_test},
+ values={
+ "rho": self._args.density,
+ "dt": self._args.dt,
+ "t": self.current_time,
+ "dx": 0.1,
+ "x0": wp.vec2(0.75, 0.75),
+ "lame_lambda": self.lame_lambda,
+ "lame_mu": self.lame_mu,
+ },
+ output_dtype=wp.vec2d,
+ )
+
+ # Enforce boundary condition
+ fem.dirichlet.project_system_rhs(
+ system_matrix=self._system_matrix,
+ projector_matrix=self._boundary_projector,
+ system_rhs=u_rhs,
+ )
+
+ # Solve with CG
+ x = wp.zeros_like(u_rhs)
+ err, n_iter = fem_example_utils.bsr_cg(
+ self._projected_system_matrix, b=u_rhs, x=x, tol=1.0e-16, quiet=self._quiet
+ )
+
+ # Extract result and add displacement delta to displacement
+ wp.utils.array_cast(in_array=x, out_array=self._du_field.dof_values)
+ fem.utils.array_axpy(x=self._du_field.dof_values, y=self.u_field.dof_values)
+
+ self.current_time += self._args.dt
+
+ return err, n_iter
+
+ def render(self):
+ self.renderer.add_field("solution", self.u_field)
+
+
+if __name__ == "__main__":
+ wp.init()
+ wp.set_module_options({"enable_backward": False}) # To speed-up compilation
+
+ args = Example.parser.parse_args()
+
+ sim = Example(args=args, quiet=True)
+
+ plot = fem_example_utils.Plot()
+
+ for f in range(sim._args.n_frames):
+ err, niter = sim.update()
+ print(f"Done frame {f} with CG residual {err} after {niter} iterations")
+ plot.add_field("u", sim.u_field)
+
+ plot.plot({"u": {"displacement": {}}})
diff --git a/deps/vomp/vomp/fem/fem_examples/model_problems/flow_past_cylinder.py b/deps/vomp/vomp/fem/fem_examples/model_problems/flow_past_cylinder.py
new file mode 100644
index 0000000000000000000000000000000000000000..65707bca79e24fa1e037ba23cb7023e9e39d5168
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/model_problems/flow_past_cylinder.py
@@ -0,0 +1,351 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This example solves the Navier-Stokes equations for a velocity field using semi-Lagrangian advection:
+
+Re Du / dt - Div[ 2 D(u) ] = 0 # conservation of momentum
+div u = 0 # incompressibility
+with u = 0 on the domain boundary, except at the channel outlet
+
+Time integration is performed using a BDF2 scheme as described in Section 2.5 of this document :
+https://membres-ljk.imag.fr/Pierre.Saramito/rheolef/rheolef.pdf
+
+"""
+
+import argparse
+
+import numpy as np
+
+import warp as wp
+import warp.fem as fem
+
+from warp.fem.utils import array_axpy
+
+from warp.examples.fem.utils import SaddleSystem, bsr_solve_saddle, Plot
+
+import matplotlib.pyplot as plt
+import matplotlib.animation as animation
+
+import meshio
+
+
+@fem.integrand
+def inertia_form(s: fem.Sample, u: fem.Field, v: fem.Field, dt: float):
+ return wp.dot(u(s), v(s)) / dt
+
+
+@fem.integrand
+def viscosity_form(s: fem.Sample, u: fem.Field, v: fem.Field):
+ return 2.0 * wp.ddot(fem.D(u, s), fem.D(v, s))
+
+
+@fem.integrand
+def bdf2_viscosity_and_inertia_form(
+ s: fem.Sample, u: fem.Field, v: fem.Field, dt: float, Re: float
+):
+ return 1.5 * Re * inertia_form(s, u, v, dt) + viscosity_form(s, u, v)
+
+
+@fem.integrand
+def bdf2_transported_inertia_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ u_prev: fem.Field,
+ v: fem.Field,
+ dt: float,
+ Re: float,
+):
+ pos = domain(s)
+
+ vel = u(s)
+ vel_prev = u_prev(s)
+
+ vel_star = 2.0 * vel - vel_prev
+
+ conv_pos = pos - 1.0 * vel_star * dt
+ conv_s = fem.lookup(domain, conv_pos, s)
+ conv_vel = wp.select(conv_pos[0] >= 16.0, u(conv_s), wp.vec2(0.0))
+
+ conv_pos_prev = pos - 2.0 * vel_star * dt
+ conv_s_prev = fem.lookup(domain, conv_pos_prev, s)
+ conv_vel_prev = wp.select(
+ conv_pos_prev[0] >= 16.0, u_prev(conv_s_prev), wp.vec2(0.0)
+ )
+
+ return 0.5 * Re * wp.dot(4.0 * conv_vel - conv_vel_prev, v(s)) / dt
+
+
+# Incompressibility
+@fem.integrand
+def div_form(
+ s: fem.Sample,
+ u: fem.Field,
+ q: fem.Field,
+):
+ return -q(s) * fem.div(u, s)
+
+
+# Vorticity field, for visualization
+@fem.integrand
+def curl_field(
+ s: fem.Sample,
+ u: fem.Field,
+):
+ return fem.curl(u, s)
+
+
+# No-slip boundary condition on walls, free velocity at outlet
+@fem.integrand
+def u_boundary_projector_form(
+ s: fem.Sample, domain: fem.Domain, u: fem.Field, v: fem.Field
+):
+ x = fem.position(domain, s)
+
+ if x[0] >= 16.0:
+ return 0.0
+
+ if x[0] <= 0.0:
+ return wp.dot(u(s), v(s))
+
+ n = fem.normal(domain, s)
+ return wp.dot(n, u(s)) * wp.dot(n, v(s))
+
+ # if x[1] <= 0.0 or x[1] >= 4.0:
+ # n = fem.normal(domain, s)
+ # return wp.dot(n, u(s)) * wp.dot(n, v(s))
+
+ # return wp.dot(u(s), v(s))
+
+
+@fem.integrand
+def u_boundary_value_form(
+ s: fem.Sample, domain: fem.Domain, v: fem.Field, in_velocity: float
+):
+ x = fem.position(domain, s)
+
+ if x[0] <= 0.0:
+ return in_velocity * v(s)[0]
+
+ return 0.0
+
+
+class Example:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("mesh")
+ parser.add_argument("--degree", type=int, default=3)
+ parser.add_argument("-n", "--num_frames", type=int, default=1000)
+ parser.add_argument("--Re", type=float, default=200.0)
+ parser.add_argument("--dt", type=float, default=0.025)
+
+ def __init__(self, stage=None, quiet=False, args=None, **kwargs):
+ if args is None:
+ # Read args from kwargs, add default arg values from parser
+ args = argparse.Namespace(**kwargs)
+ args = Example.parser.parse_args(args=[], namespace=args)
+ self._args = args
+ self._quiet = quiet
+
+ self.sim_dt = args.dt
+ self.current_frame = 0
+
+ # Read mesh and create FEM geometry
+ mesh: meshio.Mesh = meshio.read(args.mesh)
+ positions = wp.array(mesh.points[:, :2], dtype=wp.vec2)
+ tri_vidx = wp.array(mesh.cells_dict["triangle"], dtype=int)
+ self._geo = fem.Trimesh2D(
+ tri_vertex_indices=tri_vidx, positions=positions, build_bvh=True
+ )
+
+ # Velocity/pressure function spaces: P_k / P_{k-1}
+ u_space = fem.make_polynomial_space(
+ self._geo,
+ degree=args.degree,
+ dtype=wp.vec2,
+ element_basis=fem.ElementBasis.LAGRANGE,
+ )
+ p_space = fem.make_polynomial_space(
+ self._geo,
+ degree=args.degree - 1,
+ dtype=float,
+ element_basis=fem.ElementBasis.LAGRANGE,
+ )
+
+ # Displacement and displacement delta (i.e. velocity * dt) fields
+ self.u_field = u_space.make_field()
+ self._u_prev_field = u_space.make_field()
+ # Pressure field
+ self._p_field = p_space.make_field()
+
+ # Assemble constant matrices
+
+ # Inertia and elasticity terms
+ domain = fem.Cells(geometry=self._geo)
+ u_test = fem.make_test(space=u_space, domain=domain)
+ u_trial = fem.make_trial(space=u_space, domain=domain)
+
+ u_matrix = fem.integrate(
+ bdf2_viscosity_and_inertia_form,
+ fields={"u": u_trial, "v": u_test},
+ values={
+ "dt": args.dt,
+ "Re": self._args.Re,
+ },
+ )
+
+ # Incompressbility constraint
+ p_test = fem.make_test(space=p_space, domain=domain)
+ div_matrix = fem.integrate(div_form, fields={"u": u_trial, "q": p_test})
+
+ # Boundary conditions
+ boundary = fem.BoundarySides(self._geo)
+
+ # Build our projectors
+ u_bd_test = fem.make_test(space=u_space, domain=boundary)
+ u_bd_trial = fem.make_trial(space=u_space, domain=boundary)
+ u_bd_projector = fem.integrate(
+ u_boundary_projector_form,
+ fields={
+ "u": u_bd_trial,
+ "v": u_bd_test,
+ },
+ nodal=True,
+ )
+ u_bd_value = fem.integrate(
+ u_boundary_value_form,
+ fields={
+ "v": u_bd_test,
+ },
+ values={"in_velocity": 1.0},
+ nodal=True,
+ output_dtype=wp.vec2d,
+ )
+ fem.normalize_dirichlet_projector(u_bd_projector, u_bd_value)
+
+ # Project velocity matrix and boundary right hand side
+ u_bd_rhs = wp.zeros_like(u_bd_value)
+ fem.project_linear_system(
+ u_matrix, u_bd_rhs, u_bd_projector, u_bd_value, normalize_projector=False
+ )
+
+ # Project pressure boundary right-hand-side
+ div_bd_rhs = -div_matrix @ u_bd_value
+
+ # Project divergence matrix
+ div_matrix -= div_matrix @ u_bd_projector
+
+ # Assemble saddle system
+ self._saddle_system = SaddleSystem(u_matrix, div_matrix)
+
+ # Save boundary projector and prescribed values for later
+ self._u_bd_projector = u_bd_projector
+ self._u_bd_rhs = u_bd_rhs
+ self._u_test = u_test
+ self._div_bd_rhs = div_bd_rhs
+
+ # For visualization only
+ curl_space = fem.make_polynomial_space(
+ self._geo,
+ degree=args.degree,
+ dtype=float,
+ )
+ self.curl_field = curl_space.make_field()
+
+ def update(self):
+ # Use Lobatto quadrature for stability of semi-Lagrangian advection
+ quadrature = fem.RegularQuadrature(
+ self._u_test.domain,
+ order=2 * args.degree,
+ family=fem.Polynomial.LOBATTO_GAUSS_LEGENDRE,
+ )
+
+ u_rhs = fem.integrate(
+ bdf2_transported_inertia_form,
+ quadrature=quadrature,
+ fields={
+ "u": self.u_field,
+ "u_prev": self._u_prev_field,
+ "v": self._u_test,
+ },
+ values={"dt": self.sim_dt, "Re": self._args.Re},
+ output_dtype=wp.vec2d,
+ )
+
+ # Apply boundary conditions
+ # u_rhs = (I - P) * u_rhs + u_bd_rhs
+ wp.sparse.bsr_mv(self._u_bd_projector, x=u_rhs, y=u_rhs, alpha=-1.0, beta=1.0)
+ array_axpy(x=self._u_bd_rhs, y=u_rhs, alpha=1.0, beta=1.0)
+
+ p_rhs = self._div_bd_rhs
+
+ x_u = wp.empty_like(u_rhs)
+ x_p = wp.empty_like(p_rhs)
+ wp.utils.array_cast(out_array=x_u, in_array=self.u_field.dof_values)
+ wp.utils.array_cast(out_array=x_p, in_array=self._p_field.dof_values)
+
+ bsr_solve_saddle(
+ saddle_system=self._saddle_system,
+ tol=1.0e-8,
+ x_u=x_u,
+ x_p=x_p,
+ b_u=u_rhs,
+ b_p=p_rhs,
+ quiet=True,
+ method="bicgstab",
+ )
+
+ # Extract result
+ wp.copy(src=self.u_field.dof_values, dest=self._u_prev_field.dof_values)
+ wp.utils.array_cast(in_array=x_u, out_array=self.u_field.dof_values)
+ wp.utils.array_cast(in_array=x_p, out_array=self._p_field.dof_values)
+
+ self.current_frame += 1
+
+ return np.max(np.abs(self.u_field.dof_values.numpy()))
+
+ def render(self):
+ # Save curl data for visualization
+ fem.interpolate(curl_field, dest=self.curl_field, fields={"u": self.u_field})
+
+
+if __name__ == "__main__":
+ wp.init()
+ wp.set_module_options({"enable_backward": False})
+
+ args = Example.parser.parse_args()
+
+ example = Example(args=args)
+ plot = Plot()
+
+ for k in range(args.num_frames):
+ max_vel = example.update()
+
+ if example.current_frame % 10 == 1:
+ example.render()
+ plot.add_field("curl", example.curl_field)
+
+ print(f"Done frame {k} with maximum velocity = {max_vel}")
+
+ # plot.add_field("velocity_final", example.u_field)
+ plot.plot(
+ options={
+ "velocity_final": {"streamlines": {"density": 2}},
+ "curl": {
+ "contours": {"levels": [-10, -5, -2, -1, 1, 2, 5, 10]},
+ "clim": (-10, 10.0),
+ },
+ }
+ )
diff --git a/deps/vomp/vomp/fem/fem_examples/model_problems/magnetostatics_3d.py b/deps/vomp/vomp/fem/fem_examples/model_problems/magnetostatics_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce7c799c9501882ad65b94bb824d87278b5195eb
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/model_problems/magnetostatics_3d.py
@@ -0,0 +1,208 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+
+import warp as wp
+import warp.fem as fem
+
+import warp.examples.fem.utils as fem_example_utils
+
+mu_0 = wp.constant(np.pi * 4.0e-7) # Vacuum magnetic permeability
+mu_c = wp.constant(1.25e-6) # Copper magnetic permeability
+mu_i = wp.constant(6.0e-3) # Iron magnetic permeability
+J_0 = wp.constant(5.0e6) # Current density
+
+# Mesh
+
+res = 32
+degree = 1
+
+R = wp.constant(2.0) # domain radius
+coil_height = 0.25
+coil_internal_radius = wp.constant(0.3)
+coil_external_radius = wp.constant(0.4)
+
+core_height = 1.0
+core_radius = wp.constant(0.2)
+
+# positions, tet_vidx = fem_example_utils.gen_tetmesh(
+# bounds_lo=wp.vec3(-R, -R, -R),
+# bounds_hi=wp.vec3(R, R, R),
+# res=wp.vec3i(res, res, res),
+# )
+positions, hex_vidx = fem_example_utils.gen_hexmesh(
+ bounds_lo=wp.vec3(-R, -R, -R),
+ bounds_hi=wp.vec3(R, R, R),
+ res=wp.vec3i(res, res, res),
+)
+
+
+@wp.kernel
+def cylinderify(pos: wp.array(dtype=wp.vec3)):
+ i = wp.tid()
+ p = pos[i]
+
+ pxz = wp.vec3(p[0], 0.0, p[2])
+ pos[i] = wp.max(wp.abs(pxz)) * wp.normalize(pxz) + wp.vec3(0.0, p[1], 0.0)
+
+
+wp.launch(cylinderify, dim=positions.shape, inputs=[positions])
+
+# geo = fem.Tetmesh(tet_vertex_indices=tet_vidx, positions=positions)
+geo = fem.Hexmesh(hex_vertex_indices=hex_vidx, positions=positions)
+# geo = fem.Grid3D(
+# bounds_lo=wp.vec3(-R, -R, -R),
+# bounds_hi=wp.vec3(R, R, R),
+# res=wp.vec3i(res, res, res),
+# )
+
+v_space = fem.make_polynomial_space(
+ geo, degree=degree, element_basis=fem.ElementBasis.NEDELEC_FIRST_KIND, dtype=wp.vec3
+)
+
+
+@wp.func
+def mu(pos: wp.vec3):
+ x = wp.abs(pos[0])
+ y = wp.abs(pos[1])
+ z = wp.abs(pos[2])
+
+ r = wp.sqrt(x * x + z * z)
+
+ if r <= core_radius:
+ return wp.select(y < core_height, mu_0, mu_i)
+
+ if r >= coil_internal_radius and r <= coil_external_radius:
+ return wp.select(y < coil_height, mu_0, mu_c)
+
+ return mu_0
+
+
+@wp.func
+def j(pos: wp.vec3):
+ x = pos[0]
+ y = wp.abs(pos[1])
+ z = pos[2]
+
+ r = wp.sqrt(x * x + z * z)
+
+ return wp.select(
+ y < coil_height and r >= coil_internal_radius and r <= coil_external_radius,
+ wp.vec3(0.0),
+ wp.vec3(z, 0.0, -x) * J_0 / r,
+ )
+
+
+@fem.integrand
+def curl_curl_form(s: fem.Sample, domain: fem.Domain, u: fem.Field, v: fem.Field):
+ return wp.dot(fem.curl(u, s), fem.curl(v, s)) / mu(domain(s))
+
+
+@fem.integrand
+def j_form(s: fem.Sample, domain: fem.Domain, v: fem.Field):
+ return wp.dot(j(domain(s)), v(s))
+
+
+@fem.integrand
+def bd_proj_form(s: fem.Sample, domain: fem.Domain, u: fem.Field, v: fem.Field):
+ nor = fem.normal(domain, s)
+ u_s = u(s)
+ v_s = v(s)
+ u_t = u_s - wp.dot(u_s, nor) * nor
+ v_t = v_s - wp.dot(v_s, nor) * nor
+
+ return wp.dot(u_t, v_t)
+
+
+domain = fem.Cells(geo)
+u = fem.make_trial(space=v_space, domain=domain)
+v = fem.make_test(space=v_space, domain=domain)
+
+quadrature = fem.RegularQuadrature(domain, order=2 * degree)
+
+lhs = fem.integrate(
+ curl_curl_form,
+ quadrature=quadrature,
+ fields={"u": u, "v": v},
+ output_dtype=float,
+ assembly="generic",
+)
+rhs = fem.integrate(
+ j_form,
+ quadrature=quadrature,
+ fields={"v": v},
+ output_dtype=v_space.dof_dtype,
+ assembly="generic",
+)
+
+
+# Dirichlet BC
+boundary = fem.BoundarySides(geo)
+u_bd = fem.make_trial(space=v_space, domain=boundary)
+v_bd = fem.make_test(space=v_space, domain=boundary)
+dirichlet_bd_proj = fem.integrate(
+ bd_proj_form, fields={"u": u_bd, "v": v_bd}, nodal=True, output_dtype=float
+)
+fem.project_linear_system(lhs, rhs, dirichlet_bd_proj)
+
+
+x = wp.zeros_like(rhs)
+fem_example_utils.bsr_cg(
+ lhs, b=rhs, x=x, tol=1.0e-3, quiet=False, method="cr", max_iters=1000
+)
+
+# make sure result is exactly zero outisde of circle
+wp.sparse.bsr_mv(dirichlet_bd_proj, x=x, y=x, alpha=-1.0, beta=1.0)
+
+renderer = fem_example_utils.Plot()
+u_field = v_space.make_field()
+wp.utils.array_cast(in_array=x, out_array=u_field.dof_values)
+
+
+@fem.integrand
+def norm_expr(s: fem.Sample, u: fem.Field):
+ return wp.length(u(s))
+
+
+@fem.integrand
+def curl_expr(s: fem.Sample, u: fem.Field):
+ return fem.curl(u, s)
+
+
+A_norm_space = fem.make_polynomial_space(
+ geo,
+ degree=degree,
+ element_basis=fem.ElementBasis.LAGRANGE,
+ discontinuous=False,
+ dtype=float,
+)
+A_norm = A_norm_space.make_field()
+
+
+B_space = fem.make_polynomial_space(
+ geo, degree=degree, element_basis=fem.ElementBasis.LAGRANGE, dtype=wp.vec3
+)
+B = B_space.make_field()
+
+fem.interpolate(norm_expr, dest=A_norm, fields={"u": u_field})
+fem.interpolate(curl_expr, dest=B, fields={"u": u_field})
+
+renderer.add_field("A", A_norm)
+renderer.add_field("B", B)
+
+renderer.plot(
+ {"A": {"contours": {}}, "B": {"streamlines": {}}, "density": 20},
+)
diff --git a/deps/vomp/vomp/fem/fem_examples/model_problems/upwind_transport_3d.py b/deps/vomp/vomp/fem/fem_examples/model_problems/upwind_transport_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..e7e81041a4e74a7a7cea33cd82cbaa57ac4d2364
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/model_problems/upwind_transport_3d.py
@@ -0,0 +1,232 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Copyright (c) 2022 NVIDIA CORPORATION. All rights reserved.
+# NVIDIA CORPORATION and its licensors retain all intellectual property
+# and proprietary rights in and to this software, related documentation
+# and any modifications thereto. Any use, reproduction, disclosure or
+# distribution of this software and related documentation without an express
+# license agreement from NVIDIA CORPORATION is strictly prohibited.
+
+###########################################################################
+# Example Convection Diffusion DG
+#
+# This example simulates a convection-diffusion PDE using Discontinuous
+# Galerkin with upwind transport and Symmetric Interior Penalty
+#
+# D phi / dt - nu d2 phi / dx^2 = 0
+###########################################################################
+
+import warp as wp
+import warp.examples.fem.utils as fem_example_utils
+import warp.fem as fem
+
+
+@fem.integrand
+def inertia_form(s: fem.Sample, phi: fem.Field, psi: fem.Field, dt: float):
+ return phi(s) * psi(s) / dt
+
+
+@wp.func
+def velocity(pos: wp.vec3, ang_vel: float):
+ center = wp.vec3(0.5)
+ offset = pos - center
+ return wp.vec3(offset[1], -offset[0], 0.0) * ang_vel
+
+
+@fem.integrand
+def initial_condition(s: fem.Sample, domain: fem.Domain):
+ x = domain(s)
+ return wp.cos(10.0 * x[0]) * wp.sin(10.0 * x[1])
+
+
+# Standard transport term, on cells' interior
+@fem.integrand
+def transport_form(
+ s: fem.Sample, domain: fem.Domain, phi: fem.Field, psi: fem.Field, ang_vel: float
+):
+ pos = domain(s)
+ vel = velocity(pos, ang_vel)
+
+ return psi(s) * wp.dot(fem.grad(phi, s), vel)
+
+
+# Upwind flux, on cell sides
+@fem.integrand
+def upwind_transport_form(
+ s: fem.Sample, domain: fem.Domain, phi: fem.Field, psi: fem.Field, ang_vel: float
+):
+ pos = domain(s)
+ vel = velocity(pos, ang_vel)
+ vel_n = wp.dot(vel, fem.normal(domain, s))
+
+ if vel_n <= 0.0 and (
+ wp.max(pos) >= 0.9999 or wp.min(pos) <= 0.0001
+ ): # boundary side
+ return phi(s) * (-psi(s) * vel_n + 0.5 * psi(s) * wp.abs(vel_n))
+
+ # interior side
+ return fem.jump(phi, s) * (
+ -fem.average(psi, s) * vel_n + 0.5 * fem.jump(psi, s) * wp.abs(vel_n)
+ )
+
+
+@wp.func
+def refinement_field(x: wp.vec3):
+ return wp.length(x - wp.vec3(0.5)) * 3.0
+
+
+class Example:
+ def __init__(
+ self,
+ quiet=False,
+ degree=2,
+ resolution=50,
+ ang_vel=1.0,
+ level_count=5,
+ ):
+ self._quiet = quiet
+
+ self.sim_dt = 1.0 / (ang_vel * 50)
+ self.current_frame = 0
+
+ # geo = fem.Grid3D(res=wp.vec3i(resolution))
+
+ # vtx, hexes = fem_example_utils.gen_hexmesh(res=wp.vec3i(resolution))
+ # geo = fem.Hexmesh(hexes, vtx)
+
+ vol = fem_example_utils.gen_volume(res=wp.vec3i(resolution))
+ # geo = fem.Nanogrid(vol)
+
+ refinement = fem.ImplicitField(
+ domain=fem.Cells(fem.Nanogrid(vol)), func=refinement_field
+ )
+ geo = fem.adaptivity.adaptive_nanogrid_from_field(
+ vol, level_count, refinement_field=refinement, grading="none"
+ )
+
+ domain = fem.Cells(geometry=geo)
+ sides = fem.Sides(geo)
+ scalar_space = fem.make_polynomial_space(
+ geo,
+ discontinuous=True,
+ degree=degree,
+ family=fem.Polynomial.LOBATTO_GAUSS_LEGENDRE,
+ )
+
+ # Assemble transport, diffusion and inertia matrices
+
+ self._test = fem.make_test(space=scalar_space, domain=domain)
+ trial = fem.make_trial(space=scalar_space, domain=domain)
+
+ matrix_inertia = fem.integrate(
+ inertia_form,
+ fields={"phi": trial, "psi": self._test},
+ values={"dt": self.sim_dt},
+ )
+
+ matrix_transport = fem.integrate(
+ transport_form,
+ fields={"phi": trial, "psi": self._test},
+ values={"ang_vel": ang_vel},
+ )
+
+ side_test = fem.make_test(space=scalar_space, domain=sides)
+ side_trial = fem.make_trial(space=scalar_space, domain=sides)
+
+ matrix_transport += fem.integrate(
+ upwind_transport_form,
+ fields={"phi": side_trial, "psi": side_test},
+ values={"ang_vel": ang_vel},
+ )
+
+ self._matrix = matrix_inertia + matrix_transport
+
+ # Initial condition
+ self._phi_field = scalar_space.make_field()
+ fem.interpolate(initial_condition, dest=self._phi_field)
+
+ self.renderer = fem_example_utils.Plot()
+ self.renderer.add_field("phi", self._phi_field)
+
+ def step(self):
+ self.current_frame += 1
+
+ rhs = fem.integrate(
+ inertia_form,
+ fields={"phi": self._phi_field, "psi": self._test},
+ values={"dt": self.sim_dt},
+ )
+
+ phi = wp.zeros_like(rhs)
+ fem_example_utils.bsr_cg(
+ self._matrix, b=rhs, x=phi, method="bicgstab", quiet=self._quiet
+ )
+
+ wp.utils.array_cast(in_array=phi, out_array=self._phi_field.dof_values)
+
+ def render(self):
+ self.renderer.begin_frame(time=self.current_frame * self.sim_dt)
+ self.renderer.add_field("phi", self._phi_field)
+ self.renderer.end_frame()
+
+
+if __name__ == "__main__":
+ import argparse
+
+ wp.set_module_options({"enable_backward": False})
+
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
+ )
+ parser.add_argument(
+ "--device", type=str, default=None, help="Override the default Warp device."
+ )
+ parser.add_argument("--resolution", type=int, default=50, help="Grid resolution.")
+ parser.add_argument(
+ "--degree", type=int, default=2, help="Polynomial degree of shape functions."
+ )
+ parser.add_argument(
+ "--level_count", type=int, default=4, help="Number of refinement levels."
+ )
+ parser.add_argument(
+ "--num_frames", type=int, default=100, help="Total number of frames."
+ )
+ parser.add_argument("--ang_vel", type=float, default=1.0, help="Angular velocity.")
+ parser.add_argument(
+ "--headless",
+ action="store_true",
+ help="Run in headless mode, suppressing the opening of any graphical windows.",
+ )
+ parser.add_argument("--quiet", action="store_true")
+
+ args = parser.parse_known_args()[0]
+
+ with wp.ScopedDevice(args.device):
+ example = Example(
+ quiet=args.quiet,
+ degree=args.degree,
+ resolution=args.resolution,
+ ang_vel=args.ang_vel,
+ level_count=args.level_count,
+ )
+
+ for k in range(args.num_frames):
+ print(f"Frame {k}:")
+ example.step()
+ example.render()
+
+ if not args.headless:
+ example.renderer.plot()
diff --git a/deps/vomp/vomp/fem/fem_examples/mpm/mpm_granular.py b/deps/vomp/vomp/fem/fem_examples/mpm/mpm_granular.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f3f62d4522733be1fd7783c0ab1b0e6910b6051
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mpm/mpm_granular.py
@@ -0,0 +1,377 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import os
+
+import numpy as np
+
+import warp as wp
+import warp.examples
+import warp.sim.render
+from warp.sim import Model, State
+
+
+from fem_examples.mpm.mpm_integrator import MPMIntegrator
+
+import pyvista
+
+
+class Example:
+ def __init__(self, args, collider: wp.Volume, stage=None):
+ builder = wp.sim.ModelBuilder()
+ Example.emit_particles(builder, args)
+
+ builder.set_ground_plane(
+ offset=(
+ np.min(collider.points.numpy()[:, 1])
+ if collider
+ else np.min(builder.particle_q[:, 1])
+ )
+ )
+
+ model: Model = builder.finalize()
+
+ model.gravity = wp.vec3(args.gravity)
+
+ self.frame_dt = 1.0 / args.fps
+ self.sim_substeps = args.substeps
+ self.sim_dt = self.frame_dt / self.sim_substeps
+
+ self.model = model
+ self.state_0: State = model.state()
+ self.state_1: State = model.state()
+
+ self.sim_time = 0.0
+ self.integrator = MPMIntegrator(args, model, [collider] if collider else [])
+
+ self.integrator.enrich_state(self.state_0)
+ self.integrator.enrich_state(self.state_1)
+
+ self.particle_radius = self.integrator.voxel_size / 6
+
+ if args.grains:
+ self.grains = self.integrator.sample_grains(
+ self.state_0,
+ particle_radius=self.particle_radius,
+ grains_per_particle=10,
+ )
+ else:
+ self.grains = None
+
+ if stage is not None:
+ self.renderer = wp.sim.render.SimRenderer(self.model, stage)
+ else:
+ self.renderer = None
+
+ @staticmethod
+ def emit_particles(builder: wp.sim.ModelBuilder, args):
+ max_fraction = args.max_fraction
+ voxel_size = args.voxel_size
+
+ particles_per_cell = 3
+ particle_lo = np.array(args.emit_lo)
+ particle_hi = np.array(args.emit_hi)
+ particle_res = np.array(
+ np.ceil(particles_per_cell * (particle_hi - particle_lo) / voxel_size),
+ dtype=int,
+ )
+
+ Example._spawn_particles(
+ builder, particle_res, particle_lo, particle_hi, max_fraction
+ )
+
+ @staticmethod
+ def _spawn_particles(
+ builder: wp.sim.ModelBuilder, res, bounds_lo, bounds_hi, packing_fraction
+ ):
+ Nx = res[0]
+ Ny = res[1]
+ Nz = res[2]
+
+ px = np.linspace(bounds_lo[0], bounds_hi[0], Nx + 1)
+ py = np.linspace(bounds_lo[1], bounds_hi[1], Ny + 1)
+ pz = np.linspace(bounds_lo[2], bounds_hi[2], Nz + 1)
+
+ points = np.stack(np.meshgrid(px, py, pz)).reshape(3, -1).T
+
+ cell_size = (bounds_hi - bounds_lo) / res
+ cell_volume = np.prod(cell_size)
+
+ radius = np.max(cell_size) * 0.5
+ volume = np.prod(cell_volume) * packing_fraction
+
+ points += 2.0 * radius * (np.random.rand(*points.shape) - 0.5)
+ vel = np.zeros_like(points)
+
+ builder.particle_q = points
+ builder.particle_qd = vel
+ builder.particle_mass = np.full(points.shape[0], volume)
+ builder.particle_radius = np.full(points.shape[0], radius)
+ builder.particle_flags = np.zeros(points.shape[0], dtype=int)
+
+ @staticmethod
+ def add_parser_arguments(parser):
+ parser.add_argument("--emit_lo", type=float, nargs=3, default=[-10, 15, -10])
+ parser.add_argument("--emit_hi", type=float, nargs=3, default=[10, 35, 10])
+ parser.add_argument("--gravity", type=float, nargs=3, default=[0, -10, 0])
+ parser.add_argument("--fps", type=float, default=60.0)
+ parser.add_argument("--substeps", type=int, default=1)
+ parser.add_argument(
+ "--grains", action=argparse.BooleanOptionalAction, default=False
+ )
+
+ MPMIntegrator.add_parser_arguments(parser)
+
+ def update(self, frame_index):
+ with wp.ScopedTimer(f"simulate {frame_index}", active=True, synchronize=True):
+ for _s in range(self.sim_substeps):
+ self.integrator.simulate(
+ self.state_0, self.state_1, self.sim_dt, project_outside=False
+ )
+ (self.state_0, self.state_1) = (self.state_1, self.state_0)
+
+ def render(self, plotter: pyvista.Plotter):
+ with wp.ScopedTimer("render", active=True, synchronize=True):
+ time = self.sim_time
+
+ if self.renderer is not None:
+ self.renderer.begin_frame(time)
+ self.renderer.render(self.state_0)
+ self.renderer.end_frame()
+
+ if plotter is not None:
+ points = self.state_0.particle_q.numpy()
+ vel = np.linalg.norm(self.state_0.particle_qd.numpy(), axis=1)
+
+ if self.grains is None:
+ plotter.add_points(
+ points,
+ name="particles",
+ style="points",
+ render_points_as_spheres=True,
+ scalars=vel,
+ show_scalar_bar=False,
+ )
+ else:
+ self.integrator.update_grains(
+ self.state_1,
+ self.state_0,
+ self.grains,
+ self.particle_radius,
+ self.sim_dt,
+ )
+ plotter.add_points(
+ self.grains.flatten().numpy(),
+ name="grains",
+ render_points_as_spheres=True,
+ # color="b",
+ point_size=2,
+ )
+
+ # if self.velocity_field:
+ # field = self.velocity_field
+ # cells, types = field.space.vtk_cells()
+ # node_pos = field.space.node_positions().numpy()
+
+ # grid = pyvista.UnstructuredGrid(cells, types, node_pos)
+ # # grid.point_data["v"] = field.dof_values.numpy()
+
+ # plotter.add_mesh(
+ # grid,
+ # name="grid",
+ # show_edges=True,
+ # use_transparency=True,
+ # opacity=0.75,
+ # )
+ self.sim_time += self.frame_dt
+
+
+@wp.kernel
+def _fill_triangle_indices(
+ face_offsets: wp.array(dtype=int),
+ face_vertex_indices: wp.array(dtype=int),
+ tri_vertex_indices: wp.array(dtype=int),
+):
+ fid = wp.tid()
+
+ if fid == 0:
+ beg = 0
+ else:
+ beg = face_offsets[fid - 1]
+ end = face_offsets[fid]
+
+ for t in range(beg, end - 2):
+ tri_index = t - 2 * fid
+ tri_vertex_indices[3 * tri_index + 0] = face_vertex_indices[beg]
+ tri_vertex_indices[3 * tri_index + 1] = face_vertex_indices[t + 1]
+ tri_vertex_indices[3 * tri_index + 2] = face_vertex_indices[t + 2]
+
+
+def mesh_triangle_indices(face_index_counts, face_indices):
+ face_count = len(face_index_counts)
+
+ face_offsets = np.cumsum(face_index_counts)
+ tot_index_count = int(face_offsets[-1])
+
+ tri_count = tot_index_count - 2 * face_count
+ tri_index_count = 3 * tri_count
+
+ face_offsets = wp.array(face_offsets, dtype=int)
+ face_indices = wp.array(face_indices, dtype=int)
+
+ tri_indices = wp.empty(tri_index_count, dtype=int)
+
+ wp.launch(
+ kernel=_fill_triangle_indices,
+ dim=face_count,
+ inputs=[face_offsets, face_indices, tri_indices],
+ )
+
+ return tri_indices
+
+
+def load_collider_mesh(stage_path, prim_path):
+ # Create collider mesh
+ from pxr import Usd, UsdGeom
+
+ collider_stage = Usd.Stage.Open(stage_path)
+ usd_mesh = UsdGeom.Mesh(collider_stage.GetPrimAtPath(prim_path))
+ usd_counts = np.array(usd_mesh.GetFaceVertexCountsAttr().Get())
+ usd_indices = np.array(usd_mesh.GetFaceVertexIndicesAttr().Get())
+
+ collider_points = wp.array(usd_mesh.GetPointsAttr().Get(), dtype=wp.vec3)
+ collider_indices = mesh_triangle_indices(usd_counts, usd_indices)
+ return wp.Mesh(collider_points, collider_indices)
+
+
+def _create_collider_mesh(kind: str):
+ if kind == "rocks":
+ collider_stage_path = os.path.join(
+ warp.examples.get_asset_directory(), "rocks.usd"
+ )
+ collider_prim_path = "/root/rocks"
+ return load_collider_mesh(collider_stage_path, collider_prim_path)
+
+ if kind == "wedge":
+ cube_faces = np.array(
+ [
+ [0, 2, 6, 4],
+ [1, 5, 7, 3],
+ [0, 4, 5, 1],
+ [2, 3, 7, 6],
+ [0, 1, 3, 2],
+ [4, 6, 7, 5],
+ ]
+ )
+
+ # Generate cube vertex positions and rotate them by 45 degrees along z
+ cube_points = np.array(
+ [
+ [0, 0, 0],
+ [0, 0, 1],
+ [0, 1, 0],
+ [0, 1, 1],
+ [1, 0, 0],
+ [1, 0, 1],
+ [1, 1, 0],
+ [1, 1, 1],
+ ]
+ )
+ cube_points = (cube_points * [10, 10, 25]) @ np.array(
+ [
+ [np.cos(np.pi / 4), -np.sin(np.pi / 4), 0],
+ [np.sin(np.pi / 4), np.cos(np.pi / 4), 0],
+ [0, 0, 1],
+ ]
+ )
+ cube_points = cube_points + np.array([-9, 0, -12])
+
+ cube_indices = mesh_triangle_indices(np.full(6, 4), cube_faces.flatten())
+
+ return wp.Mesh(
+ wp.array(cube_points, dtype=wp.vec3), wp.array(cube_indices, dtype=int)
+ )
+
+ return None
+
+
+if __name__ == "__main__":
+ wp.set_module_options({"enable_backward": False})
+ wp.set_module_options({"fast_math": True})
+ wp.set_module_options({"max_unroll": 2})
+ wp.config.fast_math = True
+ # wp.verify_cuda = True
+
+ wp.init()
+
+ parser = argparse.ArgumentParser()
+
+ Example.add_parser_arguments(parser)
+
+ parser.add_argument("--stage_path", type=str, default=None)
+ parser.add_argument("--collider", choices=["rocks", "wedge", ""], default=None)
+ parser.add_argument("--frame_count", type=int, default=-1)
+
+ args = parser.parse_args()
+
+ collider_mesh = _create_collider_mesh(args.collider)
+ example = Example(args, collider=collider_mesh, stage=args.stage_path)
+
+ plotter = pyvista.Plotter()
+ plotter.set_background(color="white")
+ example.render(plotter)
+
+ # Add mesh for visualization
+ if collider_mesh is not None:
+ try:
+ usd_points = collider_mesh.points.numpy()
+ usd_counts = np.full(collider_mesh.indices.shape[0] // 3, 3)
+ usd_indices = collider_mesh.indices.numpy()
+
+ offsets = np.cumsum(usd_counts)
+ ranges = np.array([offsets - usd_counts, offsets]).T
+ faces = np.concatenate(
+ [
+ [count] + list(usd_indices[beg:end])
+ for (count, (beg, end)) in zip(usd_counts, ranges)
+ ]
+ )
+ ref_geom = pyvista.PolyData(usd_points, faces)
+ plotter.add_mesh(ref_geom)
+ except Exception:
+ pass
+
+ plotter.view_xy()
+ cpos = plotter.camera_position
+ plotter.camera_position = [
+ (cpos[0][0], cpos[0][1], 1.5 * cpos[0][2]),
+ cpos[1],
+ cpos[2],
+ ]
+
+ plotter.show(interactive_update=True)
+
+ frame = 0
+ while not plotter.iren.interactor.GetDone():
+ if args.frame_count < 0 or frame <= args.frame_count:
+ frame += 1
+ example.update(frame)
+ example.render(plotter)
+ plotter.screenshot(f"screenshot_{frame:04}.png")
+ plotter.update()
+
+ if example.renderer is not None:
+ example.renderer.save()
diff --git a/deps/vomp/vomp/fem/fem_examples/mpm/mpm_integrator.py b/deps/vomp/vomp/fem/fem_examples/mpm/mpm_integrator.py
new file mode 100644
index 0000000000000000000000000000000000000000..5fd3725a9d545b5094916e02da5ebe4fc4f374db
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mpm/mpm_integrator.py
@@ -0,0 +1,1614 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List
+import argparse
+from enum import Enum
+
+import numpy as np
+
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+from warp.sim import Model, State
+
+
+from fem_examples.mpm.solve_rheology import solve_rheology, solve_coulomb_isotropic
+
+vec6 = wp.vec(length=6, dtype=wp.float32)
+mat66 = wp.mat(shape=(6, 6), dtype=wp.float32)
+mat63 = wp.mat(shape=(6, 3), dtype=wp.float32)
+mat36 = wp.mat(shape=(3, 6), dtype=wp.float32)
+
+VOLUME_CUTOFF = wp.constant(1.0e-4)
+COLLIDER_EXTRAPOLATION_DISTANCE = wp.constant(0.25)
+COLLIDER_PROJECTION_THRESHOLD = wp.constant(0.5)
+
+INF_MASS = wp.constant(1.0e12)
+
+
+_FLOOR_ID = -1
+_NULL_COLLIDER_ID = -2
+_FLOOR_FRICTION = 1.0
+_DEFAULT_FRICTION = 0.0
+_DEFAULT_THICKNESS = 0.5
+
+SMALL_STRAINS = True
+
+
+@wp.struct
+class Collider:
+ meshes: wp.array(dtype=wp.uint64)
+ thicknesses: wp.array(dtype=float)
+ projection_threshold: wp.array(dtype=float)
+ friction: wp.array(dtype=float)
+ masses: wp.array(dtype=float)
+ query_max_dist: float
+ floor_y: float
+ floor_normal: wp.vec3
+
+
+@wp.func
+def collision_sdf(x: wp.vec3, collider: Collider):
+ floor_sdf = wp.dot(x, collider.floor_normal) - collider.floor_y
+
+ min_sdf = floor_sdf
+ sdf_grad = collider.floor_normal
+ sdf_vel = wp.vec3(0.0)
+ collider_id = int(_FLOOR_ID)
+
+ for m in range(collider.meshes.shape[0]):
+ mesh = collider.meshes[m]
+
+ # Union of mesh-based sdf and floor
+ query = wp.mesh_query_point_sign_normal(mesh, x, collider.query_max_dist)
+
+ if query.result:
+ cp = wp.mesh_eval_position(mesh, query.face, query.u, query.v)
+
+ offset = x - cp
+ d = wp.length(offset) * query.sign
+ sdf = d - collider.thicknesses[m]
+
+ if sdf < min_sdf:
+ min_sdf = sdf
+ if wp.abs(d) < 0.0001:
+ sdf_grad = wp.mesh_eval_face_normal(mesh, query.face)
+ else:
+ sdf_grad = wp.normalize(offset) * query.sign
+
+ sdf_vel = wp.mesh_eval_velocity(mesh, query.face, query.u, query.v)
+ collider_id = m
+
+ return min_sdf, sdf_grad, sdf_vel, collider_id
+
+
+@wp.func
+def collision_is_active(sdf: float, voxel_size: float):
+ return sdf < COLLIDER_EXTRAPOLATION_DISTANCE * voxel_size
+
+
+@fem.integrand
+def collider_volume(
+ s: fem.Sample,
+ domain: fem.Domain,
+ collider: Collider,
+ voxel_size: float,
+ volumes: wp.array(dtype=float),
+ cells: wp.array(dtype=int),
+):
+ x = domain(s)
+
+ sdf, sdf_gradient, sdf_vel, collider_id = collision_sdf(x, collider)
+ bc_active = collision_is_active(sdf, voxel_size)
+
+ if bc_active:
+ cells[s.element_index] = 1
+ if collider_id >= 0:
+ wp.atomic_add(volumes, collider_id, fem.measure(domain, s) * s.qp_weight)
+
+
+@wp.func
+def collider_friction_coefficient(collider_id: int, collider: Collider):
+ if collider_id == _FLOOR_ID:
+ return _FLOOR_FRICTION
+ return collider.friction[collider_id]
+
+
+@wp.func
+def collider_density(
+ collider_id: int, collider: Collider, collider_volumes: wp.array(dtype=float)
+):
+ if collider_id == _FLOOR_ID:
+ return INF_MASS
+ return collider.masses[collider_id] / collider_volumes[collider_id]
+
+
+@wp.func
+def collider_projection_threshold(collider_id: int, collider: Collider):
+ if collider_id == _FLOOR_ID:
+ return COLLIDER_PROJECTION_THRESHOLD
+ return collider.projection_threshold[collider_id]
+
+
+@wp.func
+def collider_is_dynamic(collider_id: int, collider: Collider):
+ if collider_id == _FLOOR_ID:
+ return False
+ return collider.masses[collider_id] < INF_MASS
+
+
+@fem.integrand
+def integrate_fraction(
+ s: fem.Sample, phi: fem.Field, domain: fem.Domain, inv_cell_volume: float
+):
+ return phi(s) * inv_cell_volume
+
+
+@fem.integrand
+def integrate_collider_fraction(
+ s: fem.Sample,
+ domain: fem.Domain,
+ phi: fem.Field,
+ collider: Collider,
+ inv_cell_volume: float,
+):
+ # Ignore space inside collider
+ sdf, sdf_gradient, sdf_vel, _id = collision_sdf(domain(s), collider)
+ return phi(s) * wp.where(sdf <= 0.0, inv_cell_volume, 0.0)
+
+
+@fem.integrand
+def integrate_velocity(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ velocities: wp.array(dtype=wp.vec3),
+ velocity_gradients: wp.array(dtype=wp.mat33),
+ dt: float,
+ gravity: wp.vec3,
+ inv_cell_volume: float,
+):
+ # APIC velocity prediction
+ node_offset = domain(fem.at_node(u, s)) - domain(s)
+ vel_apic = velocities[s.qp_index] + velocity_gradients[s.qp_index] * node_offset
+ vel_adv = vel_apic + dt * gravity
+
+ return wp.dot(u(s), vel_adv) * inv_cell_volume
+
+
+@fem.integrand
+def update_particles(
+ s: fem.Sample,
+ grid_vel: fem.Field,
+ grid_strain: fem.Field,
+ grid_strain_delta: fem.Field,
+ dt: float,
+ pos: wp.array(dtype=wp.vec3),
+ pos_prev: wp.array(dtype=wp.vec3),
+ transform: wp.array(dtype=wp.mat33),
+ transform_prev: wp.array(dtype=wp.mat33),
+ vel: wp.array(dtype=wp.vec3),
+ vel_grad: wp.array(dtype=wp.mat33),
+ elastic_strain_prev: wp.array(dtype=wp.mat33),
+ elastic_strain: wp.array(dtype=wp.mat33),
+):
+ # Advect particles and project if necessary
+
+ p_vel = grid_vel(s)
+ p_vel_grad = fem.grad(grid_vel, s)
+
+ pos_adv = pos_prev[s.qp_index] + dt * p_vel
+
+ flip = 0.95
+ strain = grid_strain(s)
+ strain_delta = grid_strain_delta(s)
+ # strain_delta = dt * fem.D(grid_vel, s)
+
+ strain = strain * (1.0 - flip) + flip * (
+ strain_delta + elastic_strain_prev[s.qp_index]
+ )
+
+ if SMALL_STRAINS:
+ # Jaumann convected derivative
+ skew = 0.5 * (p_vel_grad - wp.transpose(p_vel_grad))
+ strain += dt * (skew * strain - strain * skew)
+ strain = 0.5 * (strain + wp.transpose(strain))
+
+ pos[s.qp_index] = pos_adv
+ vel[s.qp_index] = p_vel
+ vel_grad[s.qp_index] = p_vel_grad
+
+ F_prev = transform_prev[s.qp_index]
+ # dX1/dx = dX1/dX0 dX0/dx
+ F = F_prev + dt * p_vel_grad @ F_prev
+
+ # F = F_prev + dt * (p_vel_grad @ F_prev + F_prev @ p_vel_grad)
+
+ # clamp eigenvalues of F
+ U = wp.mat33()
+ S = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, S, V)
+ S = wp.max(wp.min(S, wp.vec3(2.0)), wp.vec3(0.25))
+ F = U @ wp.diag(S) @ wp.transpose(V)
+
+ # F = wp.identity(n = 3, dtype=float) + dt*fem.grad(grid_vel, s)
+ transform[s.qp_index] = F
+
+ elastic_strain[s.qp_index] = strain
+
+
+@wp.kernel
+def project_outside_collider(
+ positions: wp.array(dtype=wp.vec3),
+ velocities: wp.array(dtype=wp.vec3),
+ velocity_gradients: wp.array(dtype=wp.mat33),
+ collider: Collider,
+ voxel_size: float,
+ dt: float,
+):
+ i = wp.tid()
+ pos_adv = positions[i]
+ p_vel = velocities[i]
+
+ # project outside of collider
+ sdf, sdf_gradient, sdf_vel, collider_id = collision_sdf(pos_adv, collider)
+
+ sdf_end = (
+ sdf
+ - wp.dot(sdf_vel, sdf_gradient) * dt
+ + collider_projection_threshold(collider_id, collider) * voxel_size
+ )
+ if sdf_end < 0:
+ # remove normal vel
+ friction = collider_friction_coefficient(collider_id, collider)
+ delta_vel = (
+ solve_coulomb_isotropic(friction, sdf_gradient, p_vel - sdf_vel)
+ + sdf_vel
+ - p_vel
+ )
+
+ p_vel += delta_vel
+ pos_adv += delta_vel * dt
+
+ # project out
+ pos_adv -= (
+ wp.min(0.0, sdf_end + dt * wp.dot(delta_vel, sdf_gradient)) * sdf_gradient
+ ) # delta_vel * dt
+
+ positions[i] = pos_adv
+ velocities[i] = p_vel
+
+ # kill velocity gradient (could maybe keep rigid)
+ velocity_gradients[i] = wp.mat33(0.0)
+
+
+@fem.integrand
+def collider_velocity(
+ s: fem.Sample,
+ domain: fem.Domain,
+ particle_density: float,
+ collider: Collider,
+ voxel_size: float,
+ node_volume: wp.array(dtype=float),
+ collider_volumes: wp.array(dtype=float),
+ collider_normals: wp.array(dtype=wp.vec3),
+ collider_friction: wp.array(dtype=float),
+ collider_ids: wp.array(dtype=int),
+ collider_impulse: wp.array(dtype=wp.vec3),
+ collider_inv_mass_matrix: wp.array(dtype=float),
+):
+ x = domain(s)
+ sdf, sdf_gradient, sdf_vel, collider_id = collision_sdf(x, collider)
+ bc_active = collision_is_active(sdf, voxel_size)
+
+ if not bc_active:
+ collider_normals[s.qp_index] = wp.vec3(0.0)
+ collider_friction[s.qp_index] = -1.0
+ collider_inv_mass_matrix[s.qp_index] = 0.0
+ collider_ids[s.qp_index] = _NULL_COLLIDER_ID
+ collider_impulse[s.qp_index] = wp.vec3(0.0)
+ return wp.vec3(0.0)
+
+ if collider_is_dynamic(collider_id, collider):
+ bc_vol = node_volume[s.qp_index]
+ bc_density = collider_density(collider_id, collider, collider_volumes)
+ bc_mass = bc_vol * bc_density
+ collider_inv_mass_matrix[s.qp_index] = particle_density / bc_mass
+ else:
+ collider_inv_mass_matrix[s.qp_index] = 0.0
+
+ collider_ids[s.qp_index] = collider_id
+ collider_normals[s.qp_index] = sdf_gradient
+ collider_friction[s.qp_index] = collider_friction_coefficient(collider_id, collider)
+
+ return sdf_vel
+
+
+@fem.integrand
+def free_velocity(
+ s: fem.Sample,
+ velocity_int: wp.array(dtype=wp.vec3),
+ particle_volume: wp.array(dtype=float),
+ inv_mass_matrix: wp.array(dtype=float),
+):
+ pvol = particle_volume[s.qp_index]
+ inv_particle_volume = 1.0 / wp.max(pvol, VOLUME_CUTOFF)
+
+ vel = velocity_int[s.qp_index] * inv_particle_volume
+ inv_mass_matrix[s.qp_index] = inv_particle_volume
+
+ return vel
+
+
+@fem.integrand
+def strain_form(
+ s: fem.Sample,
+ u: fem.Field,
+ tau: fem.Field,
+ elastic_strain: wp.array(dtype=vec6),
+ rotation: wp.array(dtype=wp.quatf),
+ dt: float,
+ domain: fem.Domain,
+ inv_cell_volume: float,
+):
+ # get polar decomposition at strain node
+ tau_index = fem.operator.node_index(tau, s)
+ R = wp.quat_to_matrix(rotation[tau_index])
+ dS = wp.transpose(R) @ fem.grad(u, s) @ R
+
+ S = fem.SymmetricTensorMapper.dof_to_value_3d(elastic_strain[tau_index])
+ dS += dS @ S
+
+ return wp.ddot(dS, tau(s)) * (dt * inv_cell_volume)
+
+
+@fem.integrand
+def integrate_elastic_strain(
+ s: fem.Sample,
+ elastic_strains: wp.array(dtype=wp.mat33),
+ tau: fem.Field,
+ domain: fem.Domain,
+ inv_cell_volume: float,
+):
+ return wp.ddot(elastic_strains[s.qp_index], tau(s)) * inv_cell_volume
+
+
+@wp.kernel
+def add_unilateral_strain_offset(
+ max_fraction: float,
+ compliance: float,
+ particle_volume: wp.array(dtype=float),
+ collider_volume: wp.array(dtype=float),
+ node_volume: wp.array(dtype=float),
+ prev_symmetric_strain: wp.array(dtype=vec6),
+ int_symmetric_strain: wp.array(dtype=vec6),
+):
+ i = wp.tid()
+
+ spherical_part = (
+ max_fraction * (node_volume[i] - collider_volume[i]) - particle_volume[i]
+ )
+ spherical_part = wp.max(spherical_part, 0.0)
+
+ strain_offset = spherical_part / 3.0 * wp.identity(n=3, dtype=float)
+ strain_offset += strain_offset * fem.SymmetricTensorMapper.dof_to_value_3d(
+ prev_symmetric_strain[i]
+ )
+
+ int_symmetric_strain[i] += fem.SymmetricTensorMapper.value_to_dof_3d(strain_offset)
+
+
+@wp.func
+def polar_decomposition(F: wp.mat33):
+ U = wp.mat33()
+ sig = wp.vec3()
+ V = wp.mat33()
+ wp.svd3(F, U, sig, V)
+
+ Vt = wp.transpose(V)
+ R = U * Vt
+ S = V * wp.diag(sig) * Vt
+
+ return R, S
+
+
+@wp.kernel
+def scale_yield_stress_and_stress_matrices(
+ yield_stress: wp.vec3,
+ stress_strain_mat: mat66,
+ particle_volume: wp.array(dtype=float),
+ node_volume: wp.array(dtype=float),
+ scaled_yield_stress: wp.array(dtype=wp.vec3),
+ scaled_mat: wp.array(dtype=mat66),
+):
+ i = wp.tid()
+
+ # Option 1: constitutive relation with particle stressa
+ pvol = wp.max(particle_volume[i], VOLUME_CUTOFF)
+
+ scaled_yield_stress[i] = yield_stress
+ scaled_mat[i] = stress_strain_mat * pvol
+
+ # Option 2: constitutive relation with Cauchy stress
+ # the stress that we compute is actually scaled by 1/fraction,
+ # so do the same thing for the yield stress
+
+ # fraction = wp.clamp(particle_volume[i] / node_volume[i], VOLUME_CUTOFF, 1.0)
+ # scaled_yield_stress[i] = yield_stress / fraction
+
+ # # we solve want eps = K cauchy, but we solve tau, sig,
+ # # with tau = V eps and phi sig = cauchy
+ # # so tau = V K cauchy = (V phi K) sig
+ # scaled_mat[i] = stress_strain_mat * fraction * wp.max(particle_volume[i], VOLUME_CUTOFF)
+
+
+@wp.kernel
+def elastic_strain_rotation(
+ int_elastic_strain: wp.array(dtype=wp.mat33),
+ particle_volume: wp.array(dtype=float),
+ strain_rotation: wp.array(dtype=wp.quatf),
+ int_symmetric_strain: wp.array(dtype=vec6),
+ symmetric_strain: wp.array(dtype=vec6),
+):
+ i = wp.tid()
+
+ Id = wp.identity(n=3, dtype=float)
+ V = particle_volume[i]
+ dF = int_elastic_strain[i]
+
+ F = dF / wp.max(V, VOLUME_CUTOFF) + Id
+ R, S = polar_decomposition(F)
+
+ strain_rotation[i] = wp.quat_from_matrix(R)
+
+ int_symmetric_strain[i] = fem.SymmetricTensorMapper.value_to_dof_3d(
+ wp.transpose(R) @ (dF + V * Id) - V * Id
+ )
+ if SMALL_STRAINS:
+ symmetric_strain[i] = vec6(0.0)
+ else:
+ symmetric_strain[i] = fem.SymmetricTensorMapper.value_to_dof_3d(S - Id)
+
+
+@wp.kernel
+def compute_elastic_strain_delta(
+ particle_volume: wp.array(dtype=float),
+ strain_rotation: wp.array(dtype=wp.quatf),
+ sym_elastic_strain: wp.array(dtype=vec6),
+ full_strain: wp.array(dtype=wp.mat33),
+ elastic_strain: wp.array(dtype=wp.mat33),
+ elastic_strain_delta: wp.array(dtype=wp.mat33),
+):
+ i = wp.tid()
+
+ V_inv = 1.0 / wp.max(particle_volume[i], VOLUME_CUTOFF)
+ dFe_prev = elastic_strain[i] * V_inv
+ dSe = fem.SymmetricTensorMapper.dof_to_value_3d(sym_elastic_strain[i]) * V_inv
+
+ Id = wp.identity(n=3, dtype=float)
+ R = wp.quat_to_matrix(strain_rotation[i])
+
+ if SMALL_STRAINS:
+ dR = wp.mat33(0.0)
+ else:
+ RtdF = full_strain[i] * V_inv
+ dR = 0.5 * (RtdF - wp.transpose(RtdF))
+
+ dFe = R @ (Id + dSe + dR) - Id
+
+ elastic_strain_delta[i] = dFe - dFe_prev
+ elastic_strain[i] = dFe
+
+ # elastic_strain_delta[i] = R @ (Id + dS - S) @ S
+ # elastic_strain_delta[i] = R @ dS
+
+
+@wp.kernel
+def fill_collider_rigidity_matrices(
+ node_positions: wp.array(dtype=wp.vec3),
+ collider_volumes: wp.array(dtype=float),
+ node_volumes: wp.array(dtype=float),
+ collider: Collider,
+ voxel_size: float,
+ collider_ids: wp.array(dtype=int),
+ collider_coms: wp.array(dtype=wp.vec3),
+ collider_inv_inertia: wp.array(dtype=wp.mat33),
+ J_rows: wp.array(dtype=int),
+ J_cols: wp.array(dtype=int),
+ J_values: wp.array(dtype=wp.mat33),
+ IJtm_values: wp.array(dtype=wp.mat33),
+ non_rigid_diagonal: wp.array(dtype=wp.mat33),
+):
+ i = wp.tid()
+ x = node_positions[i]
+
+ collider_id = collider_ids[i]
+ bc_active = collider_id != _NULL_COLLIDER_ID
+
+ cvol = voxel_size * voxel_size * voxel_size
+
+ if bc_active and collider_is_dynamic(collider_id, collider):
+ J_rows[2 * i] = i
+ J_rows[2 * i + 1] = i
+ J_cols[2 * i] = 2 * collider_id
+ J_cols[2 * i + 1] = 2 * collider_id + 1
+
+ W = wp.skew(collider_coms[collider_id] - x)
+ I = wp.identity(n=3, dtype=float)
+ J_values[2 * i] = W
+ J_values[2 * i + 1] = I
+
+ bc_mass = (
+ node_volumes[i]
+ * cvol
+ * collider_density(collider_id, collider, collider_volumes)
+ )
+
+ IJtm_values[2 * i] = -bc_mass * collider_inv_inertia[collider_id] * W
+ IJtm_values[2 * i + 1] = bc_mass / collider.masses[collider_id] * I
+
+ non_rigid_diagonal[i] = -I
+
+ else:
+ J_cols[2 * i] = -1
+ J_cols[2 * i + 1] = -1
+ J_rows[2 * i] = -1
+ J_rows[2 * i + 1] = -1
+
+ non_rigid_diagonal[i] = wp.mat33(0.0)
+
+
+@wp.kernel
+def extract_rotation_and_scales(
+ transform: wp.array(dtype=wp.mat33),
+ rotation: wp.array(dtype=wp.vec4h),
+ scales: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+
+ A = transform[i]
+
+ Q = wp.mat33()
+ R = wp.mat33()
+ wp.qr3(A, Q, R)
+
+ q = wp.quat_from_matrix(Q)
+
+ rotation[i] = wp.vec4h(wp.vec4(q[0], q[1], q[2], q[3]))
+ scales[i] = wp.vec3(wp.length(R[0]), wp.length(R[1]), wp.length(R[2]))
+
+
+@wp.kernel
+def sample_grains(
+ particles: wp.array(dtype=wp.vec3),
+ radius: float,
+ positions: wp.array2d(dtype=wp.vec3),
+):
+ pid, k = wp.tid()
+
+ rng = wp.rand_init(pid * positions.shape[1] + k)
+
+ pos_loc = (
+ 2.0
+ * wp.vec3(wp.randf(rng) - 0.5, wp.randf(rng) - 0.5, wp.randf(rng) - 0.5)
+ * radius
+ )
+ positions[pid, k] = particles[pid] + pos_loc
+
+
+@wp.kernel
+def transform_grains(
+ particle_pos_prev: wp.array(dtype=wp.vec3),
+ particle_transform_prev: wp.array(dtype=wp.mat33),
+ particle_pos: wp.array(dtype=wp.vec3),
+ particle_transform: wp.array(dtype=wp.mat33),
+ positions: wp.array2d(dtype=wp.vec3),
+):
+ pid, k = wp.tid()
+
+ pos_adv = positions[pid, k]
+
+ p_pos = particle_pos[pid]
+ p_frame = particle_transform[pid]
+ p_pos_prev = particle_pos_prev[pid]
+ p_frame_prev = particle_transform_prev[pid]
+
+ pos_loc = wp.inverse(p_frame_prev) @ (pos_adv - p_pos_prev)
+
+ p_pos_adv = p_frame @ pos_loc + p_pos
+ positions[pid, k] = p_pos_adv
+
+
+@fem.integrand
+def advect_grains(
+ s: fem.Sample,
+ domain: fem.Domain,
+ grid_vel: fem.Field,
+ dt: float,
+ positions: wp.array(dtype=wp.vec3),
+):
+ x = domain(s)
+ vel = grid_vel(s)
+ pos_adv = x + dt * vel
+ positions[s.qp_index] = pos_adv
+
+
+@wp.kernel
+def advect_grains_from_particles(
+ dt: float,
+ particle_pos_prev: wp.array(dtype=wp.vec3),
+ particle_pos: wp.array(dtype=wp.vec3),
+ particle_vel_grad: wp.array(dtype=wp.mat33),
+ positions: wp.array2d(dtype=wp.vec3),
+):
+ pid, k = wp.tid()
+
+ p_pos = particle_pos[pid]
+ p_pos_prev = particle_pos_prev[pid]
+
+ pos_loc = positions[pid, k] - p_pos_prev
+
+ p_vel_grad = particle_vel_grad[pid]
+
+ displ = dt * p_vel_grad * pos_loc + (p_pos - p_pos_prev)
+ positions[pid, k] += displ
+
+
+@wp.kernel
+def project_grains(
+ radius: float,
+ particle_pos: wp.array(dtype=wp.vec3),
+ particle_frames: wp.array(dtype=wp.mat33),
+ positions: wp.array2d(dtype=wp.vec3),
+):
+ pid, k = wp.tid()
+
+ pos_adv = positions[pid, k]
+
+ p_pos = particle_pos[pid]
+ p_frame = particle_frames[pid]
+
+ # keep within source particle
+ # pos_loc = wp.inverse(p_frame) @ (pos_adv - p_pos)
+ # dist = wp.max(wp.abs(pos_loc))
+ # if dist > radius:
+ # pos_loc = pos_loc / dist * radius
+ # p_pos_adv = p_frame @ pos_loc + p_pos
+
+ p_frame = (radius * radius) * p_frame * wp.transpose(p_frame)
+ pos_loc = pos_adv - p_pos
+ vn = wp.max(1.0, wp.dot(pos_loc, wp.inverse(p_frame) * pos_loc))
+ p_pos_adv = pos_loc / wp.sqrt(vn) + p_pos
+
+ positions[pid, k] = p_pos_adv
+
+
+@wp.kernel
+def pad_voxels(
+ particle_q: wp.array(dtype=wp.vec3i), padded_q: wp.array4d(dtype=wp.vec3i)
+):
+ pid = wp.tid()
+
+ for i in range(3):
+ for j in range(3):
+ for k in range(3):
+ padded_q[pid, i, j, k] = particle_q[pid] + wp.vec3i(i - 1, j - 1, k - 1)
+
+
+@wp.func
+def positive_mod3(x: int):
+ return (x % 3 + 3) % 3
+
+
+@wp.kernel
+def node_color_27_stencil(
+ voxels: wp.array2d(dtype=int),
+ colors: wp.array(dtype=int),
+ color_indices: wp.array(dtype=int),
+):
+ pid = wp.tid()
+
+ c = voxels[pid]
+ colors[pid] = (
+ positive_mod3(c[0]) * 9 + positive_mod3(c[1]) * 3 + positive_mod3(c[2])
+ )
+ color_indices[pid] = pid
+
+
+@wp.kernel
+def node_color_8_stencil(
+ voxels: wp.array2d(dtype=int),
+ colors: wp.array(dtype=int),
+ color_indices: wp.array(dtype=int),
+):
+ pid = wp.tid()
+
+ c = voxels[pid]
+ colors[pid] = ((c[0] & 1) << 2) + ((c[1] & 1) << 1) + (c[2] & 1)
+ color_indices[pid] = pid
+
+
+def allocate_by_voxels(particle_q, voxel_size, padded=True):
+ volume = wp.Volume.allocate_by_voxels(
+ voxel_points=particle_q.flatten(),
+ voxel_size=voxel_size,
+ )
+ if not padded:
+ return volume
+
+ voxels = wp.empty((volume.get_voxel_count(),), dtype=wp.vec3i)
+ volume.get_voxels(voxels)
+
+ padded_voxels = wp.zeros((voxels.shape[0], 3, 3, 3), dtype=wp.vec3i)
+ wp.launch(pad_voxels, voxels.shape[0], (voxels, padded_voxels))
+
+ volume = wp.Volume.allocate_by_voxels(
+ voxel_points=padded_voxels.flatten(),
+ voxel_size=voxel_size,
+ )
+
+ return volume
+
+
+class MPMIntegrator:
+ def __init__(
+ self,
+ args,
+ model: Model,
+ colliders: List[wp.Mesh] = None,
+ collider_thicknesses: List[float] = None,
+ collider_projection_threshold: List[float] = None,
+ collider_masses: List[float] = None,
+ collider_friction: List[float] = None,
+ ):
+ self.density = args.density
+ self.friction_coeff = args.friction
+ self.yield_stresses = wp.vec3(
+ args.yield_stress,
+ -args.stretching_yield_stress,
+ args.compression_yield_stress,
+ )
+
+ self.unilateral = args.unilateral
+ self.max_fraction = args.max_fraction
+
+ self.max_iterations = args.max_iters
+ self.tolerance = args.tol
+
+ self.voxel_size = args.voxel_size
+ self.degree = 1 if args.unilateral else 0
+
+ self.pad_grid = args.pad_grid
+ self.coloring = args.gs
+
+ self.compliance = args.compliance
+ poisson = args.poisson
+ lame = 1.0 / (1.0 + poisson) * np.array([poisson / (1.0 - 2.0 * poisson), 0.5])
+ K = args.compliance
+ self.stress_strain_mat = mat66(K / (2.0 * lame[1]) * np.eye(6))
+ # self.stress_strain_mat = mat66(K * np.zeros((6, 6)))
+ self.stress_strain_mat[0, 0] = K / (2.0 * lame[1] + 3.0 * lame[0])
+
+ collider = Collider()
+ collider.meshes = wp.array(
+ [collider.id for collider in colliders], dtype=wp.uint64
+ )
+ collider.thicknesses = (
+ wp.full(
+ len(collider.meshes), _DEFAULT_THICKNESS * self.voxel_size, dtype=float
+ )
+ if collider_thicknesses is None
+ else wp.array(collider_thicknesses, dtype=float)
+ )
+ collider.friction = (
+ wp.full(len(collider.meshes), _DEFAULT_FRICTION, dtype=float)
+ if collider_friction is None
+ else wp.array([bc for bc in collider_friction], dtype=float)
+ )
+ collider.masses = (
+ wp.full(len(collider.meshes), INF_MASS, dtype=float)
+ if collider_masses is None
+ else wp.array(collider_masses, dtype=float)
+ )
+ collider.projection_threshold = (
+ wp.full(len(collider.meshes), COLLIDER_PROJECTION_THRESHOLD, dtype=float)
+ if collider_projection_threshold is None
+ else wp.array(collider_projection_threshold, dtype=float)
+ )
+ collider.query_max_dist = 4.0 * self.voxel_size
+ collider.floor_y = model.ground_plane.numpy()[3] if model.ground else -1.0e8
+ collider.floor_normal = wp.vec3(model.ground_plane.numpy()[:3])
+ self._has_compliant_bodies = (
+ len(collider.masses) > 0 and np.min(collider.masses.numpy()) < INF_MASS
+ )
+
+ self.collider_coms = wp.zeros(len(collider.meshes), dtype=wp.vec3)
+ self.collider_inv_inertia = wp.zeros(len(collider.meshes), dtype=wp.mat33)
+
+ # collider.floor_normal = wp.vec3(0.0)
+ # collider.floor_normal[model.up_axis] = 1
+ self.collider = collider
+
+ # Warp.sim model
+ self.model = model
+ print("Particle count:", self.model.particle_count)
+
+ self.stress_field = None
+ self.velocity_field = None
+ self.impulse_field = None
+
+ self._collider_ids = None
+ self._strain_matrix = sp.bsr_zeros(0, 0, mat63)
+ self._transposed_strain_matrix = sp.bsr_zeros(0, 0, mat36)
+
+ self.temporary_store = fem.TemporaryStore()
+ fem.set_default_temporary_store(self.temporary_store)
+
+ @staticmethod
+ def enrich_state(state: State):
+ state.particle_qd_grad = wp.zeros(state.particle_qd.shape[0], dtype=wp.mat33)
+ state.particle_elastic_strain = wp.zeros(
+ state.particle_qd.shape[0], dtype=wp.mat33
+ )
+ state.particle_transform = wp.empty(state.particle_qd.shape[0], dtype=wp.mat33)
+ state.particle_transform.fill_(wp.mat33(np.eye(3)))
+
+ @staticmethod
+ def add_parser_arguments(parser):
+ parser.add_argument("--density", type=float, default=1.0)
+ parser.add_argument("--max_fraction", type=float, default=1.0)
+
+ parser.add_argument("--compliance", type=float, default=0.0)
+ parser.add_argument("--poisson", type=float, default=0.3)
+ parser.add_argument("--friction", type=float, default=0.48)
+ parser.add_argument("--yield_stress", "-ys", type=float, default=0.0)
+ parser.add_argument(
+ "--compression_yield_stress", "-cys", type=float, default=1.0e8
+ )
+ parser.add_argument(
+ "--stretching_yield_stress", "-sys", type=float, default=1.0e8
+ )
+ parser.add_argument(
+ "--unilateral", action=argparse.BooleanOptionalAction, default=True
+ )
+ parser.add_argument(
+ "--pad_grid", action=argparse.BooleanOptionalAction, default=False
+ )
+ parser.add_argument("--gs", action=argparse.BooleanOptionalAction, default=True)
+
+ parser.add_argument("--max_iters", type=int, default=250)
+ parser.add_argument("--tol", type=float, default=1.0e-5)
+ parser.add_argument("--voxel_size", type=float, default=1.0)
+
+ def simulate(
+ self, state_0: State, state_1: State, dt: float, project_outside: bool = True
+ ):
+ fined_grained_timers = True
+ timers_use_nvtx = True
+
+ with wp.ScopedTimer(
+ "Allocate grid",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ volume = allocate_by_voxels(
+ state_0.particle_q, self.voxel_size, padded=self.pad_grid
+ )
+ grid = fem.Nanogrid(volume)
+
+ domain = fem.Cells(grid)
+ inv_cell_volume = 1.0 / self.voxel_size**3
+
+ # Define function spaces: linear (Q1) for velocity and volume fraction,
+ # piecewise-constant for pressure
+ velocity_basis = fem.make_polynomial_basis_space(grid, degree=1)
+ strain_basis = fem.make_polynomial_basis_space(
+ grid,
+ self.degree,
+ # discontinuous=True,
+ # element_basis=fem.ElementBasis.NONCONFORMING_POLYNOMIAL,
+ )
+
+ velocity_space = fem.make_collocated_function_space(
+ velocity_basis, dtype=wp.vec3
+ )
+ fraction_space = fem.make_collocated_function_space(velocity_basis, dtype=float)
+ full_strain_space = fem.make_collocated_function_space(
+ strain_basis, dtype=wp.mat33
+ )
+ rotation_space = fem.make_collocated_function_space(
+ strain_basis, dtype=wp.quatf
+ )
+ sym_strain_space = fem.make_collocated_function_space(
+ strain_basis,
+ dof_mapper=fem.SymmetricTensorMapper(
+ dtype=wp.mat33, mapping=fem.SymmetricTensorMapper.Mapping.DB16
+ ),
+ )
+ divergence_space = fem.make_collocated_function_space(strain_basis, dtype=float)
+
+ with wp.ScopedTimer(
+ "Create fields",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ velocity_test = fem.make_test(velocity_space)
+ velocity_trial = fem.make_trial(velocity_space)
+ fraction_test = fem.make_test(
+ fraction_space,
+ space_restriction=velocity_test.space_restriction,
+ )
+
+ sym_strain_test = fem.make_test(sym_strain_space)
+ full_strain_test = fem.make_test(
+ full_strain_space, space_restriction=sym_strain_test.space_restriction
+ )
+
+ velocity_field = velocity_space.make_field()
+ self.velocity_field = velocity_field
+
+ impulse_field = velocity_space.make_field()
+ if self.impulse_field is not None:
+ prev_impulse_field = fem.NonconformingField(domain, self.impulse_field)
+ fem.interpolate(prev_impulse_field, dest=impulse_field)
+ self.impulse_field = impulse_field
+
+ # Interpolate previous stress
+ stress_field = sym_strain_space.make_field()
+ if self.stress_field is not None:
+ prev_stress_field = fem.NonconformingField(domain, self.stress_field)
+ fem.interpolate(prev_stress_field, dest=stress_field)
+ self.stress_field = stress_field
+
+ elastic_strain_field = full_strain_space.make_field()
+ elastic_strain_delta_field = full_strain_space.make_field()
+ int_symmetric_strain_field = sym_strain_space.make_field()
+ elastic_rotation_field = rotation_space.make_field()
+
+ collider_velocity_field = velocity_space.make_field()
+
+ # Bin particles to grid cells
+ with wp.ScopedTimer(
+ "Bin particles",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ pic = fem.PicQuadrature(
+ domain=domain,
+ positions=state_0.particle_q,
+ measures=self.model.particle_mass,
+ )
+
+ vel_node_count = velocity_space.node_count()
+ strain_node_count = sym_strain_space.node_count()
+
+ # Velocity right-hand side
+ with wp.ScopedTimer(
+ "Free velocity",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ velocity_int = fem.integrate(
+ integrate_velocity,
+ quadrature=pic,
+ fields={"u": velocity_test},
+ values={
+ "velocities": state_0.particle_qd,
+ "velocity_gradients": state_0.particle_qd_grad,
+ "dt": dt,
+ "gravity": self.model.gravity,
+ "inv_cell_volume": inv_cell_volume,
+ },
+ output_dtype=wp.vec3,
+ )
+ particle_volume = fem.integrate(
+ integrate_fraction,
+ quadrature=pic,
+ fields={"phi": fraction_test},
+ values={"inv_cell_volume": inv_cell_volume},
+ output_dtype=float,
+ )
+
+ inv_mass_matrix = fem.borrow_temporary(
+ self.temporary_store, shape=(vel_node_count,), dtype=float
+ )
+
+ fem.interpolate(
+ free_velocity,
+ dest=fem.make_restriction(
+ velocity_field, space_restriction=velocity_test.space_restriction
+ ),
+ values={
+ "velocity_int": velocity_int,
+ "particle_volume": particle_volume,
+ "inv_mass_matrix": inv_mass_matrix.array,
+ },
+ )
+
+ with wp.ScopedTimer(
+ "collider",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ # Accumulate collider volume so we can distribute mass
+ # and record cells with collider to build subdomain
+
+ collider_total_volumes = fem.borrow_temporary(
+ self.temporary_store, shape=(self.collider.meshes.shape[0]), dtype=float
+ )
+ collider_total_volumes.array.zero_()
+
+ collider_cells = fem.borrow_temporary(
+ self.temporary_store, shape=(domain.element_count(),), dtype=int
+ )
+ collider_cells.array.zero_()
+
+ collider_quadrature_order = self.degree + 1
+ collider_quadrature = fem.RegularQuadrature(
+ domain=domain,
+ order=collider_quadrature_order,
+ family=fem.Polynomial.LOBATTO_GAUSS_LEGENDRE,
+ )
+ fem.interpolate(
+ collider_volume,
+ quadrature=collider_quadrature,
+ values={
+ "collider": self.collider,
+ "volumes": collider_total_volumes.array,
+ "cells": collider_cells.array,
+ "voxel_size": self.voxel_size,
+ },
+ )
+
+ node_volume = fem.integrate(
+ integrate_fraction,
+ fields={"phi": fraction_test},
+ values={"inv_cell_volume": inv_cell_volume},
+ output_dtype=float,
+ )
+
+ collider_normal = fem.borrow_temporary(
+ self.temporary_store, shape=(vel_node_count,), dtype=wp.vec3
+ )
+ collider_friction = fem.borrow_temporary(
+ self.temporary_store, shape=(vel_node_count,), dtype=float
+ )
+ self._collider_ids = wp.empty(vel_node_count, dtype=int)
+
+ collider_inv_mass_matrix = fem.borrow_temporary(
+ self.temporary_store, shape=(vel_node_count,), dtype=float
+ )
+
+ fem.interpolate(
+ collider_velocity,
+ dest=fem.make_restriction(
+ collider_velocity_field,
+ space_restriction=velocity_test.space_restriction,
+ ),
+ values={
+ "particle_density": self.density,
+ "node_volume": node_volume,
+ "voxel_size": self.voxel_size,
+ "collider_volumes": collider_total_volumes.array,
+ "collider": self.collider,
+ "collider_inv_mass_matrix": collider_inv_mass_matrix.array,
+ "collider_normals": collider_normal.array,
+ "collider_friction": collider_friction.array,
+ "collider_ids": self._collider_ids,
+ "collider_impulse": self.impulse_field.dof_values,
+ },
+ )
+
+ with wp.ScopedTimer(
+ "Rigidity",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ rigidity_matrix = self.build_rigidity_matrix(
+ collider_total_volumes.array, node_volume
+ )
+
+ # Scale plastic / elastic parameters with volume fraction
+ with wp.ScopedTimer(
+ "Parameter scaling",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ divergence_test = fem.make_test(
+ divergence_space,
+ space_restriction=sym_strain_test.space_restriction,
+ )
+
+ particle_volume = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count, dtype=float
+ )
+ node_volume = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count, dtype=float
+ )
+ node_collider_volume = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count, dtype=float
+ )
+
+ fem.integrate(
+ integrate_fraction,
+ quadrature=pic,
+ fields={"phi": divergence_test},
+ values={"inv_cell_volume": inv_cell_volume},
+ output=particle_volume.array,
+ )
+ fem.integrate(
+ integrate_fraction,
+ fields={"phi": divergence_test},
+ values={"inv_cell_volume": inv_cell_volume},
+ output=node_volume.array,
+ )
+ fem.integrate(
+ integrate_collider_fraction,
+ quadrature=fem.RegularQuadrature(
+ domain=domain,
+ order=collider_quadrature_order,
+ family=fem.Polynomial.LOBATTO_GAUSS_LEGENDRE,
+ ),
+ fields={"phi": divergence_test},
+ values={
+ "collider": self.collider,
+ "inv_cell_volume": inv_cell_volume,
+ },
+ output=node_collider_volume.array,
+ )
+
+ scaled_yield_stress = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count, dtype=wp.vec3
+ )
+ scaled_stress_strain_mat = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count, dtype=mat66
+ )
+
+ wp.launch(
+ kernel=scale_yield_stress_and_stress_matrices,
+ dim=strain_node_count,
+ inputs=[
+ self.yield_stresses / self.density,
+ self.stress_strain_mat,
+ particle_volume.array,
+ node_volume.array,
+ scaled_yield_stress.array,
+ scaled_stress_strain_mat.array,
+ ],
+ )
+
+ with wp.ScopedTimer(
+ "Strain rhs",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ fem.integrate(
+ integrate_elastic_strain,
+ quadrature=pic,
+ fields={"tau": full_strain_test},
+ values={
+ "elastic_strains": state_0.particle_elastic_strain,
+ "inv_cell_volume": inv_cell_volume,
+ },
+ output=elastic_strain_field.dof_values,
+ )
+
+ prev_symmetric_strain = wp.empty_like(int_symmetric_strain_field.dof_values)
+ wp.launch(
+ elastic_strain_rotation,
+ dim=strain_node_count,
+ inputs=[
+ elastic_strain_field.dof_values,
+ particle_volume.array,
+ elastic_rotation_field.dof_values,
+ int_symmetric_strain_field.dof_values,
+ prev_symmetric_strain,
+ ],
+ )
+
+ # Strain matrix
+ with wp.ScopedTimer(
+ "Strain matrix",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ sp.bsr_set_zero(
+ self._strain_matrix,
+ rows_of_blocks=strain_node_count,
+ cols_of_blocks=vel_node_count,
+ )
+ fem.integrate(
+ strain_form,
+ quadrature=pic,
+ fields={
+ "u": velocity_trial,
+ "tau": sym_strain_test,
+ },
+ values={
+ "dt": dt,
+ "inv_cell_volume": inv_cell_volume,
+ "elastic_strain": prev_symmetric_strain,
+ "rotation": elastic_rotation_field.dof_values,
+ },
+ output_dtype=float,
+ output=self._strain_matrix,
+ )
+ # self._strain_matrix.nnz_sync()
+
+ self._strain_matrix.nnz_sync()
+
+ if self.unilateral:
+ with wp.ScopedTimer(
+ "Offset",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ wp.launch(
+ add_unilateral_strain_offset,
+ dim=strain_node_count,
+ inputs=[
+ self.max_fraction,
+ self.compliance,
+ particle_volume.array,
+ node_collider_volume.array,
+ node_volume.array,
+ prev_symmetric_strain,
+ int_symmetric_strain_field.dof_values,
+ ],
+ )
+
+ with wp.ScopedTimer(
+ "Strain solve",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ color_offsets, color_indices = self._compute_coloring(
+ grid, strain_node_count
+ )
+
+ solve_rheology(
+ self.unilateral,
+ self.friction_coeff,
+ self.max_iterations,
+ self.tolerance,
+ self._strain_matrix,
+ self._transposed_strain_matrix,
+ inv_mass_matrix.array,
+ scaled_yield_stress.array,
+ scaled_stress_strain_mat.array,
+ int_symmetric_strain_field.dof_values,
+ stress_field.dof_values,
+ velocity_field.dof_values,
+ collider_friction.array,
+ collider_normal.array,
+ collider_velocity_field.dof_values,
+ collider_inv_mass_matrix.array,
+ self.impulse_field.dof_values,
+ color_offsets=color_offsets,
+ color_indices=None if color_indices is None else color_indices.array,
+ rigidity_mat=rigidity_matrix,
+ temporary_store=self.temporary_store,
+ )
+
+ if color_indices is not None:
+ color_indices.release()
+
+ full_strain = fem.integrate(
+ strain_form,
+ quadrature=pic,
+ fields={
+ "u": velocity_field,
+ "tau": full_strain_test,
+ },
+ values={
+ "dt": dt,
+ "inv_cell_volume": inv_cell_volume,
+ "elastic_strain": prev_symmetric_strain,
+ "rotation": elastic_rotation_field.dof_values,
+ },
+ output_dtype=wp.mat33,
+ )
+
+ wp.launch(
+ compute_elastic_strain_delta,
+ dim=strain_node_count,
+ inputs=[
+ particle_volume.array,
+ elastic_rotation_field.dof_values,
+ int_symmetric_strain_field.dof_values,
+ full_strain,
+ elastic_strain_field.dof_values,
+ elastic_strain_delta_field.dof_values,
+ ],
+ )
+
+ # (A)PIC advection
+ with wp.ScopedTimer(
+ "Advection",
+ active=fined_grained_timers,
+ use_nvtx=timers_use_nvtx,
+ synchronize=True,
+ ):
+ fem.interpolate(
+ update_particles,
+ quadrature=pic,
+ values={
+ "pos": state_1.particle_q,
+ "pos_prev": state_0.particle_q,
+ "vel": state_1.particle_qd,
+ "vel_grad": state_1.particle_qd_grad,
+ "transform": state_1.particle_transform,
+ "transform_prev": state_0.particle_transform,
+ "dt": dt,
+ "elastic_strain_prev": state_0.particle_elastic_strain,
+ "elastic_strain": state_1.particle_elastic_strain,
+ },
+ fields={
+ "grid_vel": velocity_field,
+ "grid_strain": elastic_strain_field,
+ "grid_strain_delta": elastic_strain_delta_field,
+ },
+ )
+
+ if project_outside:
+ self.project_outside(state_1, dt)
+
+ def build_rigidity_matrix(self, collider_volumes, node_volumes):
+ if not self._has_compliant_bodies:
+ return None
+
+ vel_node_count = self.velocity_field.space.node_count()
+ collider_count = self.collider.meshes.shape[0]
+
+ J_rows = wp.empty(vel_node_count * 2, dtype=int)
+ J_cols = wp.empty(vel_node_count * 2, dtype=int)
+ J_values = wp.empty(vel_node_count * 2, dtype=wp.mat33)
+ IJtm_values = wp.empty(vel_node_count * 2, dtype=wp.mat33)
+ Iphi_diag = wp.empty(vel_node_count, dtype=wp.mat33)
+
+ with wp.ScopedTimer("Fill rigidity matrix", synchronize=True, active=False):
+ wp.launch(
+ fill_collider_rigidity_matrices,
+ dim=vel_node_count,
+ inputs=[
+ self.velocity_field.space.node_positions(),
+ collider_volumes,
+ node_volumes,
+ self.collider,
+ self.voxel_size,
+ self._collider_ids,
+ self.collider_coms,
+ self.collider_inv_inertia,
+ J_rows,
+ J_cols,
+ J_values,
+ IJtm_values,
+ Iphi_diag,
+ ],
+ )
+
+ with wp.ScopedTimer("Build rigidity matrix", synchronize=True, active=False):
+ J = sp.bsr_from_triplets(
+ rows_of_blocks=vel_node_count,
+ cols_of_blocks=2 * collider_count,
+ rows=J_rows,
+ columns=J_cols,
+ values=J_values,
+ )
+
+ IJtm = sp.bsr_from_triplets(
+ cols_of_blocks=vel_node_count,
+ rows_of_blocks=2 * collider_count,
+ columns=J_rows,
+ rows=J_cols,
+ values=IJtm_values,
+ )
+
+ with wp.ScopedTimer("Assemble rigidity matrix", synchronize=True, active=False):
+ Iphi = sp.bsr_diag(Iphi_diag)
+ Iphi = sp.bsr_from_triplets(
+ rows_of_blocks=Iphi.nrow,
+ cols_of_blocks=Iphi.ncol,
+ columns=Iphi.columns,
+ rows=Iphi.uncompress_rows(),
+ values=Iphi.values,
+ )
+ rigid = Iphi + J @ IJtm
+ rigid.nnz_sync()
+
+ return rigid
+
+ def collect_collider_impulses(self):
+ x = self.velocity_field.space.node_positions()
+
+ collider_impulse = wp.zeros_like(self.impulse_field.dof_values)
+ cell_volume = self.voxel_size**3
+ fem.utils.array_axpy(
+ y=collider_impulse,
+ x=self.impulse_field.dof_values,
+ alpha=-self.density * cell_volume,
+ beta=0.0,
+ )
+
+ return self._collider_ids, collider_impulse, x
+
+ def project_outside(self, state: State, dt: float):
+ wp.launch(
+ project_outside_collider,
+ dim=state.particle_count,
+ inputs=[
+ state.particle_q,
+ state.particle_qd,
+ state.particle_qd_grad,
+ self.collider,
+ self.voxel_size,
+ dt,
+ ],
+ )
+
+ def extract_rotation_and_scales(self, state: State):
+ particle_transform_rotation = wp.empty(state.particle_count, dtype=wp.vec4h)
+ particle_transform_scales = wp.empty(state.particle_count, dtype=wp.vec3)
+
+ wp.launch(
+ extract_rotation_and_scales,
+ dim=state.particle_count,
+ inputs=[
+ state.particle_transform,
+ particle_transform_rotation,
+ particle_transform_scales,
+ ],
+ )
+
+ return particle_transform_rotation, particle_transform_scales
+
+ def sample_grains(
+ self, state: State, particle_radius: float, grains_per_particle: int
+ ):
+ grains = wp.empty((state.particle_count, grains_per_particle), dtype=wp.vec3)
+
+ wp.launch(
+ sample_grains,
+ dim=grains.shape,
+ inputs=[
+ state.particle_q,
+ particle_radius,
+ grains,
+ ],
+ )
+
+ return grains
+
+ def update_grains(
+ self,
+ state_prev: State,
+ state: State,
+ grains: wp.array,
+ particle_radius: float,
+ dt: float,
+ ):
+ if self.velocity_field is None:
+ return
+
+ grain_pos = grains.flatten()
+ domain = fem.Cells(self.velocity_field.space.geometry)
+ grain_pic = fem.PicQuadrature(domain, positions=grain_pos)
+
+ # wp.launch(
+ # transform_grains,
+ # dim=grains.shape,
+ # inputs=[
+ # state_prev.particle_q,
+ # state_prev.particle_transform,
+ # state.particle_q,
+ # state.particle_transform,
+ # grains,
+ # ],
+ # )
+
+ wp.launch(
+ advect_grains_from_particles,
+ dim=grains.shape,
+ inputs=[
+ dt,
+ state_prev.particle_q,
+ state.particle_q,
+ state.particle_qd_grad,
+ grains,
+ ],
+ )
+
+ fem.interpolate(
+ advect_grains,
+ quadrature=grain_pic,
+ values={
+ "dt": dt,
+ "positions": grain_pos,
+ },
+ fields={
+ "grid_vel": self.velocity_field,
+ },
+ )
+
+ wp.launch(
+ project_grains,
+ dim=grains.shape,
+ inputs=[
+ particle_radius,
+ state.particle_q,
+ state.particle_transform,
+ grains,
+ ],
+ )
+
+ def _compute_coloring(self, grid, strain_node_count):
+ if not self.coloring:
+ return None, None
+
+ colors = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count * 2, dtype=int
+ )
+ color_indices = fem.borrow_temporary(
+ self.temporary_store, shape=strain_node_count * 2, dtype=int
+ )
+
+ if self.degree == 1:
+ voxels = grid.vertex_grid.get_voxels()
+ wp.launch(
+ node_color_27_stencil,
+ dim=strain_node_count,
+ inputs=[voxels, colors.array, color_indices.array],
+ )
+ else:
+ voxels = grid.cell_grid.get_voxels()
+ wp.launch(
+ node_color_8_stencil,
+ dim=strain_node_count,
+ inputs=[voxels, colors.array, color_indices.array],
+ )
+
+ wp.utils.radix_sort_pairs(
+ keys=colors.array,
+ values=color_indices.array,
+ count=strain_node_count,
+ )
+
+ unique_colors = colors.array[strain_node_count:]
+ color_node_counts = color_indices.array[strain_node_count:]
+ color_count = wp.utils.runlength_encode(
+ colors.array,
+ run_values=unique_colors,
+ run_lengths=color_node_counts,
+ value_count=strain_node_count,
+ )
+
+ color_offsets = np.concatenate(
+ [[0], np.cumsum(color_node_counts[:color_count].numpy())]
+ )
+
+ return color_offsets, color_indices
diff --git a/deps/vomp/vomp/fem/fem_examples/mpm/solve_rheology.py b/deps/vomp/vomp/fem/fem_examples/mpm/solve_rheology.py
new file mode 100644
index 0000000000000000000000000000000000000000..08e8a24b2253239337756c60fec7e415f79e3a97
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/mpm/solve_rheology.py
@@ -0,0 +1,778 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import gc
+import math
+from typing import Optional
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+
+from warp.fem.utils import symmetric_eigenvalues_qr
+from warp.utils import array_inner
+
+DELASSUS_DIAG_CUTOFF = wp.constant(1.0e-6)
+
+vec6 = wp.vec(length=6, dtype=wp.float32)
+mat66 = wp.mat(shape=(6, 6), dtype=wp.float32)
+mat63 = wp.mat(shape=(6, 3), dtype=wp.float32)
+mat36 = wp.mat(shape=(3, 6), dtype=wp.float32)
+
+
+@wp.kernel
+def compute_delassus_diagonal(
+ split_mass: wp.bool,
+ strain_mat_offsets: wp.array(dtype=int),
+ strain_mat_columns: wp.array(dtype=int),
+ strain_mat_values: wp.array(dtype=mat63),
+ inv_volume: wp.array(dtype=float),
+ stress_strain_matrices: wp.array(dtype=mat66),
+ transposed_strain_mat_offsets: wp.array(dtype=int),
+ strain_rhs: wp.array(dtype=vec6),
+ stress: wp.array(dtype=vec6),
+ delassus_rotation: wp.array(dtype=mat66),
+ delassus_diagonal: wp.array(dtype=vec6),
+ delassus_normal: wp.array(dtype=vec6),
+):
+ tau_i = wp.tid()
+ block_beg = strain_mat_offsets[tau_i]
+ block_end = strain_mat_offsets[tau_i + 1]
+
+ diag_block = stress_strain_matrices[tau_i]
+
+ for b in range(block_beg, block_end):
+ u_i = strain_mat_columns[b]
+
+ mass_ratio = wp.where(
+ split_mass,
+ value_if_true=float(
+ transposed_strain_mat_offsets[u_i + 1]
+ - transposed_strain_mat_offsets[u_i]
+ ),
+ value_if_false=1.0,
+ )
+
+ b_val = strain_mat_values[b]
+ inv_frac = inv_volume[u_i] * mass_ratio
+
+ diag_block += (b_val * inv_frac) @ wp.transpose(b_val)
+
+ if wp.trace(diag_block) > DELASSUS_DIAG_CUTOFF:
+ for k in range(1, 6):
+ # Remove shear-divergence coupling
+ # (current implementation of solve_coulomb_aniso normal and tangential responses are independent)
+ diag_block[0, k] = 0.0
+ diag_block[k, 0] = 0.0
+
+ diag, ev = symmetric_eigenvalues_qr(diag_block, 1.0e-12)
+ if not (wp.ddot(ev, ev) < 1.0e16 and wp.length_sq(diag) < 1.0e16):
+ # wp.print(diag_block)
+ # wp.print(diag)
+ diag = wp.get_diag(diag_block)
+ ev = wp.identity(n=6, dtype=float)
+
+ # Disable null modes -- e.g. from velocity boundary conditions
+ for k in range(0, 6):
+ if diag[k] < DELASSUS_DIAG_CUTOFF:
+ diag[k] = 1.0
+ ev[k] = vec6(0.0)
+
+ delassus_diagonal[tau_i] = diag
+ delassus_rotation[tau_i] = wp.transpose(ev) @ wp.diag(
+ wp.cw_div(vec6(1.0), diag)
+ )
+
+ # Apply rotation to contact data
+ nor = ev * vec6(1.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+ delassus_normal[tau_i] = nor
+
+ strain_rhs[tau_i] = ev * strain_rhs[tau_i]
+ stress[tau_i] = wp.cw_mul(ev * stress[tau_i], diag)
+
+ for b in range(block_beg, block_end):
+ strain_mat_values[b] = ev * strain_mat_values[b]
+
+ stress_strain_matrices[tau_i] = (
+ ev * stress_strain_matrices[tau_i] * delassus_rotation[tau_i]
+ )
+ else:
+ # Not a valid constraint, disable
+ delassus_diagonal[tau_i] = vec6(1.0)
+ delassus_normal[tau_i] = vec6(1.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+ delassus_rotation[tau_i] = wp.identity(n=6, dtype=float)
+ stress[tau_i] = vec6(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+ strain_rhs[tau_i] = vec6(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+ stress_strain_matrices[tau_i] = mat66(0.0)
+
+
+@wp.kernel
+def scale_transposed_strain_mat(
+ tr_strain_mat_offsets: wp.array(dtype=int),
+ tr_strain_mat_columns: wp.array(dtype=int),
+ tr_strain_mat_values: wp.array(dtype=mat36),
+ inv_volume: wp.array(dtype=float),
+ delassus_rotation: wp.array(dtype=mat66),
+):
+ u_i = wp.tid()
+ block_beg = tr_strain_mat_offsets[u_i]
+ block_end = tr_strain_mat_offsets[u_i + 1]
+
+ for b in range(block_beg, block_end):
+ tau_i = tr_strain_mat_columns[b]
+
+ tr_strain_mat_values[b] = (
+ inv_volume[u_i] * tr_strain_mat_values[b] @ delassus_rotation[tau_i]
+ )
+
+
+@wp.kernel
+def postprocess_stress(
+ delassus_rotation: wp.array(dtype=mat66),
+ delassus_diagonal: wp.array(dtype=vec6),
+ stress_strain_matrices: wp.array(dtype=mat66),
+ stress: wp.array(dtype=vec6),
+ elastic_strain: wp.array(dtype=vec6),
+ delta_stress: wp.array(dtype=vec6),
+):
+ i = wp.tid()
+ rot = delassus_rotation[i]
+ diag = delassus_diagonal[i]
+
+ loc_stress = stress[i]
+ loc_strain = -(stress_strain_matrices[i] * loc_stress)
+
+ stress[i] = rot * loc_stress
+ elastic_strain[i] = rot * wp.cw_mul(loc_strain, diag)
+ delta_stress[i] = wp.cw_div(loc_stress, diag)
+
+
+@wp.func
+def eval_sliding_residual(alpha: float, D: vec6, b_T: vec6):
+ d_alpha = D + vec6(alpha)
+
+ r_alpha = wp.cw_div(b_T, d_alpha)
+ dr_dalpha = -wp.cw_div(r_alpha, d_alpha)
+
+ f = wp.dot(r_alpha, r_alpha) - 1.0
+ df_dalpha = 2.0 * wp.dot(r_alpha, dr_dalpha)
+ return f, df_dalpha
+
+
+@wp.func
+def solve_sliding_aniso(D: vec6, b_T: vec6, yield_stress: float):
+ if yield_stress <= 0.0:
+ return b_T
+
+ # Viscous shear opposite to tangential stress, zero divergence
+ # find alpha, r_t, mu_rn, (D + alpha/(mu r_n) I) r_t + b_t = 0, |r_t| = mu r_n
+ # find alpha, |(D mu r_n + alpha I)^{-1} b_t|^2 = 1.0
+
+ mu_rn = yield_stress
+ Dmu_rn = D * mu_rn
+
+ alpha_0 = wp.length(b_T)
+ alpha_max = alpha_0 - wp.min(Dmu_rn)
+ alpha_min = wp.max(0.0, alpha_0 - wp.max(Dmu_rn))
+
+ # We're looking for the root of an hyperbola, approach using Newton from the left
+ alpha_cur = alpha_min
+
+ if alpha_max - alpha_min > DELASSUS_DIAG_CUTOFF:
+ for k in range(24):
+ f_cur, df_dalpha = eval_sliding_residual(alpha_cur, Dmu_rn, b_T)
+
+ alpha_next = wp.clamp(alpha_cur - f_cur / df_dalpha, alpha_min, alpha_max)
+ alpha_cur = alpha_next
+
+ u_T = wp.cw_div(b_T * alpha_cur, Dmu_rn + vec6(alpha_cur))
+
+ # Sanity checkI
+ # r_T_sol = -u_T * mu_rn / alpha_cur
+ # err = wp.length(r_T_sol) - mu_rn
+ # if err > 1.4:
+ # f_cur, df_dalpha = eval_sliding_residual(alpha_cur, Dmu_rn, b_T)
+ # wp.printf("%d %f %f %f %f %f \n", k, wp.length(r_T_sol), f_cur, mu_rn, alpha_cur / alpha_min, alpha_cur / alpha_max)
+ # #wp.print(D)
+ # #wp.print(b)
+ # #wp.print(mu_dyn)
+
+ return u_T
+
+
+@wp.func
+def solve_coulomb_aniso(
+ D: vec6,
+ b: vec6,
+ nor: vec6,
+ unilateral: wp.bool,
+ mu_st: float,
+ mu_dyn: float,
+ yield_stress: wp.vec3,
+):
+ # Note: this assumes that D.nor = lambda nor
+ # i.e. nor should be along one canonical axis
+ # (solve_sliding aniso would get a lot more complex otherwise as normal and tangential
+ # responses become interlinked)
+
+ # Positive divergence, zero stress
+ b_N = wp.dot(b, nor)
+ if unilateral and b_N >= 0.0:
+ return b
+
+ # Static friction, zero shear
+ r_0 = -wp.cw_div(b, D)
+ r_N0 = wp.dot(r_0, nor)
+ r_T = r_0 - r_N0 * nor
+
+ r_N = wp.clamp(r_N0, yield_stress[1], yield_stress[2])
+ u_N = (r_N - r_N0) * wp.cw_mul(nor, D)
+
+ mu_rn = wp.max(mu_st * r_N, yield_stress[0])
+ mu_rn_sq = mu_rn * mu_rn
+ if mu_rn >= 0 and wp.dot(r_T, r_T) <= mu_rn_sq:
+ return u_N
+
+ mu_rn = wp.max(mu_dyn * r_N, yield_stress[0])
+ b_T = b - b_N * nor
+ return u_N + solve_sliding_aniso(D, b_T, mu_rn)
+
+
+@wp.func
+def solve_local_stress(
+ tau_i: int,
+ unilateral: wp.bool,
+ friction_coeff: float,
+ D: vec6,
+ yield_stress: wp.array(dtype=wp.vec3),
+ stress_strain_matrices: wp.array(dtype=mat66),
+ strain_mat_offsets: wp.array(dtype=int),
+ strain_mat_columns: wp.array(dtype=int),
+ strain_mat_values: wp.array(dtype=mat63),
+ delassus_normal: wp.array(dtype=vec6),
+ strain_rhs: wp.array(dtype=vec6),
+ velocities: wp.array(dtype=wp.vec3),
+ stress: wp.array(dtype=vec6),
+ delta_correction: wp.array(dtype=vec6),
+):
+ block_beg = strain_mat_offsets[tau_i]
+ block_end = strain_mat_offsets[tau_i + 1]
+
+ tau = strain_rhs[tau_i]
+
+ for b in range(block_beg, block_end):
+ u_i = strain_mat_columns[b]
+ tau += strain_mat_values[b] * velocities[u_i]
+
+ nor = delassus_normal[tau_i]
+ cur_stress = stress[tau_i]
+
+ # substract elastic strain
+ # this is the one thing that spearates elasticity from simple modification
+ # of the the Delassus operator
+ tau += stress_strain_matrices[tau_i] * cur_stress
+
+ tau_new = solve_coulomb_aniso(
+ D,
+ tau - cur_stress,
+ nor,
+ unilateral,
+ friction_coeff,
+ friction_coeff,
+ yield_stress[tau_i],
+ )
+
+ delta_stress = tau_new - tau
+
+ delta_correction[tau_i] = delta_stress
+ stress[tau_i] = cur_stress + delta_stress
+
+
+@wp.kernel
+def solve_local_stress_jacobi(
+ unilateral: wp.bool,
+ friction_coeff: float,
+ yield_stress: wp.array(dtype=wp.vec3),
+ stress_strain_matrices: wp.array(dtype=mat66),
+ strain_mat_offsets: wp.array(dtype=int),
+ strain_mat_columns: wp.array(dtype=int),
+ strain_mat_values: wp.array(dtype=mat63),
+ delassus_diagonal: wp.array(dtype=vec6),
+ delassus_normal: wp.array(dtype=vec6),
+ strain_rhs: wp.array(dtype=vec6),
+ velocities: wp.array(dtype=wp.vec3),
+ stress: wp.array(dtype=vec6),
+ delta_correction: wp.array(dtype=vec6),
+):
+ tau_i = wp.tid()
+ D = delassus_diagonal[tau_i]
+
+ solve_local_stress(
+ tau_i,
+ unilateral,
+ friction_coeff,
+ D,
+ yield_stress,
+ stress_strain_matrices,
+ strain_mat_offsets,
+ strain_mat_columns,
+ strain_mat_values,
+ delassus_normal,
+ strain_rhs,
+ velocities,
+ stress,
+ delta_correction,
+ )
+
+
+@wp.func
+def apply_stress_delta_gs(
+ tau_i: int,
+ D: vec6,
+ delta_stress: vec6,
+ strain_mat_offsets: wp.array(dtype=int),
+ strain_mat_columns: wp.array(dtype=int),
+ strain_mat_values: wp.array(dtype=mat63),
+ inv_mass_matrix: wp.array(dtype=float),
+ velocities: wp.array(dtype=wp.vec3),
+):
+ block_beg = strain_mat_offsets[tau_i]
+ block_end = strain_mat_offsets[tau_i + 1]
+
+ for b in range(block_beg, block_end):
+ u_i = strain_mat_columns[b]
+ delta_vel = inv_mass_matrix[u_i] @ (
+ wp.cw_div(delta_stress, D) @ strain_mat_values[b]
+ )
+ velocities[u_i] += delta_vel
+
+
+@wp.kernel
+def apply_stress_gs(
+ color_offset: int,
+ color_indices: wp.array(dtype=int),
+ strain_mat_offsets: wp.array(dtype=int),
+ strain_mat_columns: wp.array(dtype=int),
+ strain_mat_values: wp.array(dtype=mat63),
+ delassus_diagonal: wp.array(dtype=vec6),
+ inv_mass_matrix: wp.array(dtype=float),
+ stress: wp.array(dtype=vec6),
+ velocities: wp.array(dtype=wp.vec3),
+):
+ tau_i = color_indices[wp.tid() + color_offset]
+
+ D = delassus_diagonal[tau_i]
+ cur_stress = stress[tau_i]
+
+ apply_stress_delta_gs(
+ tau_i,
+ D,
+ cur_stress,
+ strain_mat_offsets,
+ strain_mat_columns,
+ strain_mat_values,
+ inv_mass_matrix,
+ velocities,
+ )
+
+
+@wp.kernel
+def solve_local_stress_gs(
+ color_offset: int,
+ unilateral: wp.bool,
+ friction_coeff: float,
+ color_indices: wp.array(dtype=int),
+ yield_stress: wp.array(dtype=wp.vec3),
+ stress_strain_matrices: wp.array(dtype=mat66),
+ strain_mat_offsets: wp.array(dtype=int),
+ strain_mat_columns: wp.array(dtype=int),
+ strain_mat_values: wp.array(dtype=mat63),
+ delassus_diagonal: wp.array(dtype=vec6),
+ delassus_normal: wp.array(dtype=vec6),
+ inv_mass_matrix: wp.array(dtype=float),
+ strain_rhs: wp.array(dtype=vec6),
+ velocities: wp.array(dtype=wp.vec3),
+ stress: wp.array(dtype=vec6),
+ delta_correction: wp.array(dtype=vec6),
+):
+ tau_i = color_indices[wp.tid() + color_offset]
+
+ D = delassus_diagonal[tau_i]
+ solve_local_stress(
+ tau_i,
+ unilateral,
+ friction_coeff,
+ D,
+ yield_stress,
+ stress_strain_matrices,
+ strain_mat_offsets,
+ strain_mat_columns,
+ strain_mat_values,
+ delassus_normal,
+ strain_rhs,
+ velocities,
+ stress,
+ delta_correction,
+ )
+
+ apply_stress_delta_gs(
+ tau_i,
+ D,
+ delta_correction[tau_i],
+ strain_mat_offsets,
+ strain_mat_columns,
+ strain_mat_values,
+ inv_mass_matrix,
+ velocities,
+ )
+
+
+@wp.kernel
+def apply_collider_impulse(
+ collider_impulse: wp.array(dtype=wp.vec3),
+ inv_mass: wp.array(dtype=float),
+ collider_inv_mass: wp.array(dtype=float),
+ velocities: wp.array(dtype=wp.vec3),
+ collider_velocities: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+ velocities[i] += inv_mass[i] * collider_impulse[i]
+ collider_velocities[i] -= collider_inv_mass[i] * collider_impulse[i]
+
+
+@wp.func
+def solve_coulomb_isotropic(
+ mu: float,
+ nor: wp.vec3,
+ u: wp.vec3,
+):
+ u_n = wp.dot(u, nor)
+ if u_n < 0.0:
+ u -= u_n * nor
+ tau = wp.length_sq(u)
+ alpha = mu * u_n
+ if tau <= alpha * alpha:
+ u = wp.vec3(0.0)
+ else:
+ u *= 1.0 + mu * u_n / wp.sqrt(tau)
+
+ return u
+
+
+@wp.kernel
+def solve_nodal_friction(
+ inv_mass: wp.array(dtype=float),
+ collider_friction: wp.array(dtype=float),
+ collider_normals: wp.array(dtype=wp.vec3),
+ collider_inv_mass: wp.array(dtype=float),
+ velocities: wp.array(dtype=wp.vec3),
+ collider_velocities: wp.array(dtype=wp.vec3),
+ impulse: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+
+ friction_coeff = collider_friction[i]
+ if friction_coeff < 0.0:
+ return
+
+ n = collider_normals[i]
+ u0 = velocities[i] - collider_velocities[i]
+
+ w = inv_mass[i] + collider_inv_mass[i]
+
+ u = solve_coulomb_isotropic(friction_coeff, n, u0 - impulse[i] * w)
+
+ delta_u = u - u0
+ delta_impulse = delta_u / w
+
+ impulse[i] += delta_impulse
+ velocities[i] += inv_mass[i] * delta_impulse
+ collider_velocities[i] -= collider_inv_mass[i] * delta_impulse
+
+
+def solve_rheology(
+ unilateral: bool,
+ friction_coeff: float,
+ max_iterations: int,
+ tolerance: float,
+ strain_mat: sp.BsrMatrix,
+ transposed_strain_mat: sp.BsrMatrix,
+ inv_volume,
+ yield_stress,
+ stress_strain_matrices,
+ strain_rhs,
+ stress,
+ velocity,
+ collider_friction,
+ collider_normals,
+ collider_velocities,
+ collider_inv_mass,
+ collider_impulse,
+ color_offsets,
+ color_indices: wp.array = None,
+ rigidity_mat: Optional[sp.BsrMatrix] = None,
+ temporary_store: Optional[fem.TemporaryStore] = None,
+):
+ delta_stress = fem.borrow_temporary_like(stress, temporary_store)
+
+ delassus_rotation = fem.borrow_temporary(
+ temporary_store, shape=stress.shape, dtype=mat66
+ )
+ delassus_diagonal = fem.borrow_temporary(
+ temporary_store, shape=stress.shape, dtype=vec6
+ )
+ delassus_normal = fem.borrow_temporary(
+ temporary_store, shape=stress.shape, dtype=vec6
+ )
+
+ color_count = 0 if color_offsets is None else len(color_offsets) - 1
+ gs = color_count > 0
+ split_mass = not gs
+
+ # Build transposed matrix
+ if not gs:
+ sp.bsr_set_transpose(dest=transposed_strain_mat, src=strain_mat)
+
+ # Compute and factorize diagonal blacks, rotate strain matrix to diagonal basis
+ wp.launch(
+ kernel=compute_delassus_diagonal,
+ dim=stress.shape[0],
+ inputs=[
+ split_mass,
+ strain_mat.offsets,
+ strain_mat.columns,
+ strain_mat.values,
+ inv_volume,
+ stress_strain_matrices,
+ transposed_strain_mat.offsets,
+ strain_rhs,
+ stress,
+ delassus_rotation.array,
+ delassus_diagonal.array,
+ delassus_normal.array,
+ ],
+ )
+
+ if rigidity_mat is not None:
+ prev_collider_velocity = fem.borrow_temporary_like(
+ collider_velocities, temporary_store
+ )
+ wp.copy(dest=prev_collider_velocity.array, src=collider_velocities)
+
+ def apply_rigidity_matrix():
+ # Apply rigidity matrix
+ if rigidity_mat is not None:
+ # velocity delta
+ fem.utils.array_axpy(
+ y=prev_collider_velocity.array,
+ x=collider_velocities,
+ alpha=1.0,
+ beta=-1.0,
+ )
+ # rigidity contribution to new velocity
+ sp.bsr_mv(
+ A=rigidity_mat,
+ x=prev_collider_velocity.array,
+ y=collider_velocities,
+ alpha=1.0,
+ beta=1.0,
+ )
+ # save for next iterations
+ wp.copy(dest=prev_collider_velocity.array, src=collider_velocities)
+
+ if gs:
+ # Apply initial guess
+ apply_stress_launch = wp.launch(
+ kernel=apply_stress_gs,
+ dim=1,
+ inputs=[
+ 0,
+ color_indices,
+ strain_mat.offsets,
+ strain_mat.columns,
+ strain_mat.values,
+ delassus_diagonal.array,
+ inv_volume,
+ stress,
+ velocity,
+ ],
+ block_dim=64,
+ record_cmd=True,
+ )
+
+ for k in range(color_count):
+ apply_stress_launch.set_param_at_index(0, color_offsets[k])
+ apply_stress_launch.set_dim((int(color_offsets[k + 1] - color_offsets[k]),))
+ apply_stress_launch.launch()
+
+ else:
+ # Apply local scaling and rotations to transposed strain matrix
+ wp.launch(
+ kernel=scale_transposed_strain_mat,
+ dim=inv_volume.shape[0],
+ inputs=[
+ transposed_strain_mat.offsets,
+ transposed_strain_mat.columns,
+ transposed_strain_mat.values,
+ inv_volume,
+ delassus_rotation.array,
+ ],
+ )
+
+ # Apply initial guess
+ sp.bsr_mv(A=transposed_strain_mat, x=stress, y=velocity, alpha=1.0, beta=1.0)
+
+ # Apply initial contact guess
+ wp.launch(
+ kernel=apply_collider_impulse,
+ dim=collider_impulse.shape[0],
+ inputs=[
+ collider_impulse,
+ inv_volume,
+ collider_inv_mass,
+ velocity,
+ collider_velocities,
+ ],
+ )
+ apply_rigidity_matrix()
+
+ def solve_collider():
+ wp.launch(
+ kernel=solve_nodal_friction,
+ dim=collider_impulse.shape[0],
+ inputs=[
+ inv_volume,
+ collider_friction,
+ collider_normals,
+ collider_inv_mass,
+ velocity,
+ collider_velocities,
+ collider_impulse,
+ ],
+ )
+ apply_rigidity_matrix()
+
+ # Gauss-Seidel solve
+ def do_gs_batch(num_iterations):
+ solve_local_launch = wp.launch(
+ kernel=solve_local_stress_gs,
+ dim=1,
+ inputs=[
+ 0,
+ unilateral,
+ friction_coeff * math.sqrt(3.0 / 2.0),
+ color_indices,
+ yield_stress,
+ stress_strain_matrices,
+ strain_mat.offsets,
+ strain_mat.columns,
+ strain_mat.values,
+ delassus_diagonal.array,
+ delassus_normal.array,
+ inv_volume,
+ strain_rhs,
+ velocity,
+ stress,
+ delta_stress.array,
+ ],
+ block_dim=64,
+ record_cmd=True,
+ )
+
+ for i in range(num_iterations):
+ for k in range(color_count):
+ solve_local_launch.set_param_at_index(0, color_offsets[k])
+ solve_local_launch.set_dim(
+ (int(color_offsets[k + 1] - color_offsets[k]),)
+ )
+ solve_local_launch.launch()
+
+ solve_collider()
+
+ # Jacobi solve
+ def do_jacobi_batch(num_iterations):
+ solve_local_launch = wp.launch(
+ kernel=solve_local_stress_jacobi,
+ dim=stress.shape[0],
+ inputs=[
+ unilateral,
+ friction_coeff * math.sqrt(3.0 / 2.0),
+ yield_stress,
+ stress_strain_matrices,
+ strain_mat.offsets,
+ strain_mat.columns,
+ strain_mat.values,
+ delassus_diagonal.array,
+ delassus_normal.array,
+ strain_rhs,
+ velocity,
+ stress,
+ delta_stress.array,
+ ],
+ record_cmd=True,
+ )
+ for i in range(num_iterations):
+ solve_local_launch.launch()
+ # Add jacobi delta
+ sp.bsr_mv(
+ A=transposed_strain_mat,
+ x=delta_stress.array,
+ y=velocity,
+ alpha=1.0,
+ beta=1.0,
+ )
+ solve_collider()
+
+ solve_granularity = 25 if gs else 50
+ use_graph = True
+ solve_graph = None
+
+ do_batch = do_gs_batch if gs else do_jacobi_batch
+
+ if use_graph:
+ gc.collect(0)
+ gc.disable()
+ wp.capture_begin(force_module_load=False)
+ do_batch(solve_granularity)
+ gc.collect(0)
+ solve_graph = wp.capture_end()
+ gc.enable()
+
+ for batch in range(max_iterations // solve_granularity):
+ if solve_graph is None:
+ do_batch(solve_granularity)
+ else:
+ wp.capture_launch(solve_graph)
+
+ res = math.sqrt(array_inner(delta_stress.array, delta_stress.array)) / (
+ 1 + stress.shape[0]
+ )
+ print(
+ f"{'Gauss-Seidel' if gs else 'Jacobi'} iterations #{(batch+1) * solve_granularity} \t res(l2)={res}"
+ )
+ if res < tolerance:
+ break
+
+ wp.launch(
+ kernel=postprocess_stress,
+ dim=stress.shape[0],
+ inputs=[
+ delassus_rotation.array,
+ delassus_diagonal.array,
+ stress_strain_matrices,
+ stress,
+ strain_rhs,
+ delta_stress.array,
+ ],
+ )
diff --git a/deps/vomp/vomp/fem/fem_examples/simplicits/collisions.py b/deps/vomp/vomp/fem/fem_examples/simplicits/collisions.py
new file mode 100644
index 0000000000000000000000000000000000000000..d33ab6d8698c8fd1e86958c28424f95633f23ad1
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/simplicits/collisions.py
@@ -0,0 +1,399 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import warp as wp
+import warp.fem as fem
+
+from warp.sim.collide import triangle_closest_point
+
+
+@wp.kernel
+def detect_mesh_collisions(
+ max_contacts: int,
+ mesh_ids: wp.array(dtype=wp.uint64),
+ pos_cur: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ m, tid = wp.tid()
+ mesh_id = mesh_ids[m]
+
+ x = pos_cur[tid]
+
+ lower = x - wp.vec3(radius)
+ upper = x + wp.vec3(radius)
+
+ query = wp.mesh_query_aabb(mesh_id, lower, upper)
+
+ mesh = wp.mesh_get(mesh_id)
+
+ face_index = wp.int32(0)
+ while wp.mesh_query_aabb_next(query, face_index):
+ t1 = mesh.indices[face_index * 3 + 0]
+ t2 = mesh.indices[face_index * 3 + 1]
+ t3 = mesh.indices[face_index * 3 + 2]
+
+ u1 = mesh.points[t1]
+ u2 = mesh.points[t2]
+ u3 = mesh.points[t3]
+ p, bary, feature_type = triangle_closest_point(u1, u2, u3, x)
+
+ delta = x - p
+ dist = wp.length(delta) # * sign
+ penetration = radius - dist
+
+ if penetration > 0.0:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ if dist < 0.00001:
+ n = wp.mesh_eval_face_normal(mesh_id, face_index)
+ else:
+ n = wp.normalize(delta) # * sign
+ normals[idx] = n
+
+ v = wp.mesh_eval_velocity(mesh_id, face_index, bary[1], bary[2])
+
+ kinematic_gap = (dist - wp.dot(du_cur[tid], n)) * n - v
+ kinematic_gaps[idx] = kinematic_gap
+ indices_a[idx] = tid
+ indices_b[idx] = fem.NULL_QP_INDEX
+
+
+@wp.kernel
+def detect_ground_collisions(
+ max_contacts: int,
+ up_axis: int,
+ pos_cur: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ ground_height: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ i = wp.tid()
+ x = pos_cur[i]
+
+ if x[up_axis] < ground_height + radius:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ nor = wp.vec3(0.0)
+ nor[up_axis] = 1.0
+
+ normals[idx] = nor
+ kinematic_gaps[idx] = (wp.dot(x - du_cur[i], nor) - ground_height) * nor
+ indices_a[idx] = i
+ indices_b[idx] = fem.NULL_QP_INDEX
+
+
+@wp.kernel
+def detect_particle_collisions(
+ max_contacts: int,
+ grid: wp.uint64,
+ radius: float,
+ self_collision_immune_radius: float,
+ pos_cur: wp.array(dtype=wp.vec3),
+ pos_rest: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ qp_obj_ids: wp.array(dtype=int),
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ tid = wp.tid()
+
+ idx_a = wp.hash_grid_point_id(grid, tid)
+ obj_a = qp_obj_ids[idx_a]
+ pos_a = pos_cur[idx_a]
+
+ for idx_b in wp.hash_grid_query(grid, pos_a, radius):
+ if idx_a >= idx_b:
+ continue # symmetric
+
+ obj_b = qp_obj_ids[idx_b]
+ if (
+ obj_a == obj_b
+ and wp.length_sq(pos_rest[idx_a] - pos_rest[idx_b])
+ < self_collision_immune_radius
+ ):
+ continue # no self collisions
+
+ pos_b = pos_cur[idx_b]
+ d = wp.length(pos_a - pos_b)
+ if d <= radius:
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ nor = wp.normalize(pos_a - pos_b)
+ normals[idx] = nor
+ kinematic_gaps[idx] = (
+ wp.dot(pos_a - pos_b - (du_cur[idx_a] - du_cur[idx_b]), nor) * nor
+ )
+ indices_a[idx] = idx_a
+ indices_b[idx] = idx_b
+
+
+@wp.func
+def collision_offset(
+ c: int,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ idx_a = indices_a[c]
+ idx_b = indices_b[c]
+
+ offset = du_cur[idx_a] + kinematic_gaps[c]
+ if idx_b != fem.NULL_QP_INDEX:
+ offset -= du_cur[idx_b]
+
+ return offset
+
+
+@wp.func
+def collision_target_distance(
+ c: int,
+ radius: float,
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+):
+ return wp.where(indices_b[c] == fem.NULL_ELEMENT_INDEX, 1.0, 2.0) * radius
+
+
+@wp.kernel
+def collision_energy(
+ radius: float,
+ barrier_distance_ratio: float,
+ mu: float,
+ dt: float,
+ nu: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ energies: wp.array(dtype=float),
+):
+ c = wp.tid()
+
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+ rp_ratio = barrier_distance_ratio
+
+ nor = normals[c]
+ d = wp.dot(offset, nor)
+ d_hat = d / rc
+
+ # Check its within radiuses.
+ if rp_ratio < d_hat and d_hat <= 1.0:
+ d_min_l_squared = (d_hat - 1.0) * (
+ d_hat - 1.0
+ ) # quadratic term ensures energy is 0 when d = rc
+ E = -d_min_l_squared * wp.log(
+ d_hat - rp_ratio
+ ) # log barrier term. inf when two rp's overlaps.
+
+ # friction energy
+ dc = d_hat - 1.0
+ dp = d_hat - rp_ratio
+ barrier = 2.0 * wp.log(dp)
+
+ dE_d_hat = -dc * (barrier + dc / dp)
+ vt = (offset - d * nor) / dt # tangential velocity
+ vt_norm = wp.length(vt)
+
+ mu_fn = -mu * dE_d_hat / rc # yield force
+
+ E += (
+ mu_fn
+ * dt
+ * (
+ 0.5 * nu * vt_norm * vt_norm
+ + wp.where(
+ vt_norm < 1.0,
+ vt_norm * vt_norm * (1.0 - vt_norm / 3.0),
+ vt_norm - 1.0 / 3.0,
+ )
+ )
+ )
+
+ else:
+ E = 0.0
+
+ energies[c] = E
+
+
+@wp.kernel
+def collision_gradient_and_hessian(
+ radius: float,
+ barrier_distance_ratio: float,
+ mu: float,
+ dt: float,
+ nu: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ gradient: wp.array(dtype=wp.vec3),
+ hessian: wp.array(dtype=wp.mat33),
+):
+ c = wp.tid()
+
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+ rp_ratio = barrier_distance_ratio
+
+ nor = normals[c]
+ d = wp.dot(offset, nor)
+ d_hat = d / rc
+
+ if rp_ratio < d_hat and d_hat <= 1.0:
+ dc = d_hat - 1.0
+ dp = d_hat - rp_ratio
+ barrier = 2.0 * wp.log(dp)
+
+ dE_d_hat = -dc * (barrier + dc / dp)
+ gradient[c] = dE_d_hat / rc * nor
+
+ dbarrier_d_hat = 2.0 / dp
+ ddcdp_d_hat = (dp - dc) / (dp * dp)
+
+ d2E_d_hat2 = -(barrier + dc / dp) - dc * (dbarrier_d_hat + ddcdp_d_hat)
+ hessian[c] = d2E_d_hat2 / (rc * rc) * wp.outer(nor, nor)
+
+ # friction
+
+ vt = (offset - d * nor) / dt # tangential velocity
+ vt_norm = wp.length(vt)
+ vt_dir = wp.normalize(vt) # avoids dealing with 0
+
+ mu_fn = -mu * dE_d_hat / rc # yield force
+
+ f1_over_vt_norm = wp.where(vt_norm < 1.0, 2.0 - vt_norm, 1.0 / vt_norm)
+ gradient[c] += mu_fn * (f1_over_vt_norm + nu) * vt
+
+ # regularization such that f / H dt <= k v (penalizes friction switching dir)
+ friction_slip_reg = 0.1
+ df1_d_vtn = wp.max(
+ 2.0 * (1.0 - vt_norm),
+ friction_slip_reg / (0.5 * friction_slip_reg + vt_norm),
+ )
+
+ vt_perp = wp.cross(vt_dir, nor)
+ hessian[c] += (
+ mu_fn
+ / dt
+ * (
+ (df1_d_vtn + nu) * wp.outer(vt_dir, vt_dir)
+ + (f1_over_vt_norm + nu) * wp.outer(vt_perp, vt_perp)
+ )
+ )
+
+ else:
+ gradient[c] = wp.vec3(0.0)
+ hessian[c] = wp.mat33(0.0)
+
+
+@wp.kernel
+def compute_collision_bounds(
+ radius: float,
+ barrier_distance_ratio: float,
+ du_cur: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ normals: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ delta_du: wp.array(dtype=wp.vec3),
+ jacobian_a_offsets: wp.array(dtype=int),
+ jacobian_a_columns: wp.array(dtype=int),
+ jacobian_b_offsets: wp.array(dtype=int),
+ jacobian_b_columns: wp.array(dtype=int),
+ dof_t_max: wp.array(dtype=float),
+):
+ c = wp.tid()
+
+ # Distance delta
+ nor = normals[c]
+
+ idx_a = indices_a[c]
+ idx_b = indices_b[c]
+
+ delta_d_a = wp.dot(nor, delta_du[idx_a])
+ if idx_b == fem.NULL_QP_INDEX:
+ delta_d_b = 0.0
+ else:
+ delta_d_b = -wp.dot(nor, delta_du[idx_b])
+
+ # Current distance
+ offset = collision_offset(c, du_cur, kinematic_gaps, indices_a, indices_b)
+ rc = collision_target_distance(c, radius, indices_a, indices_b)
+ rp = barrier_distance_ratio * rc
+ gap_cur = rp - wp.dot(offset, nor)
+
+ if gap_cur >= 0.0:
+ # Missed due to too large timestep. Can't do anything now
+ return
+
+ MAX_PROGRESS = 0.75
+ max_delta_d = 0.5 * MAX_PROGRESS * gap_cur
+
+ # local bounds
+ if delta_d_a < 0.0: # getting closer
+ t_max = wp.clamp(max_delta_d / delta_d_a, 0.0, 1.0)
+ if t_max < 1.0:
+ dof_beg = jacobian_a_offsets[c]
+ dof_end = jacobian_a_offsets[c + 1]
+ for dof in range(dof_beg, dof_end):
+ wp.atomic_min(dof_t_max, jacobian_a_columns[dof], t_max)
+
+ if delta_d_b < 0.0: # getting closer
+ t_max = wp.clamp(max_delta_d / delta_d_b, 0.0, 1.0)
+ if t_max < 1.0:
+ dof_beg = jacobian_b_offsets[c]
+ dof_end = jacobian_b_offsets[c + 1]
+ for dof in range(dof_beg, dof_end):
+ wp.atomic_min(dof_t_max, jacobian_b_columns[dof], t_max)
+
+
+@wp.kernel
+def apply_collision_bounds(
+ delta_du: wp.array(dtype=wp.vec3),
+ alpha: float,
+ dof_t_max: wp.array(dtype=float),
+ du: wp.array(dtype=wp.vec3),
+ u: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+
+ dui = wp.min(alpha, dof_t_max[i]) * delta_du[i]
+
+ du[i] += dui
+ u[i] += dui
diff --git a/deps/vomp/vomp/fem/fem_examples/simplicits/linalg.py b/deps/vomp/vomp/fem/fem_examples/simplicits/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..6cf217cc5639246401a29596f5fb0fa9fb24191c
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/simplicits/linalg.py
@@ -0,0 +1,163 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Any
+
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+
+
+@wp.kernel
+def dense_chol_batched(
+ size: int,
+ regul: float,
+ lhs: wp.array2d(dtype=float),
+ L: wp.array2d(dtype=float),
+):
+ i = wp.tid()
+ wp.dense_chol(size, lhs[i], regul, L[i])
+
+
+@wp.kernel
+def dense_chol_subs_batched(
+ rhs: wp.array2d(dtype=float),
+ res: wp.array2d(dtype=float),
+ L: wp.array2d(dtype=float),
+):
+ i = wp.tid()
+ wp.dense_subs(rhs.shape[1], L[i], rhs[i], res[i])
+
+
+def create_batched_cholesky_kernel(num_dofs):
+ @fem.cache.dynamic_kernel(
+ suffix=num_dofs, kernel_options={"enable_backward": False}
+ )
+ def eval_tiled_dense_cholesky_batched(A: wp.array3d(dtype=float), reg: float):
+ block, _ = wp.tid()
+
+ a = wp.tile_load(
+ A[block], shape=(num_dofs, num_dofs), offset=(0, 0), storage="shared"
+ )
+ r = wp.tile_ones(dtype=float, shape=(num_dofs)) * reg
+ b = wp.tile_diag_add(a, r)
+ l = wp.tile_cholesky(b)
+ wp.tile_store(A[block], l)
+
+ return eval_tiled_dense_cholesky_batched
+
+
+def create_batched_cholesky_solve_kernel(num_dofs):
+ @fem.cache.dynamic_kernel(suffix=num_dofs)
+ def solve_tiled_dense_cholesky_batched(
+ L: wp.array3d(dtype=float),
+ X: wp.array1d(dtype=float),
+ Y: wp.array1d(dtype=float),
+ ):
+ block, _ = wp.tid()
+
+ a = wp.tile_load(L[block], shape=(num_dofs, num_dofs), storage="shared")
+ x = wp.tile_load(X, offset=block * num_dofs, shape=num_dofs)
+ y = wp.tile_cholesky_solve(a, x)
+ wp.tile_store(Y, y, offset=block * num_dofs)
+
+ return solve_tiled_dense_cholesky_batched
+
+
+@wp.kernel
+def _coarsen_structure(
+ row_ratio: int,
+ col_ratio: int,
+ src_offsets: wp.array(dtype=int),
+ src_columns: wp.array(dtype=int),
+ dst_offsets: wp.array(dtype=int),
+ dst_columns: wp.array(dtype=int),
+):
+ row = wp.tid()
+ src_beg = src_offsets[row * row_ratio]
+ beg = src_beg // (row_ratio * col_ratio)
+ end = src_offsets[(row + 1) * row_ratio] // (row_ratio * col_ratio)
+
+ dst_offsets[row + 1] = end
+ for block in range(beg, end):
+ dst_columns[block] = (
+ src_columns[src_beg + col_ratio * (block - beg)] // col_ratio
+ )
+
+
+def bsr_coarsen_aligned(src: sp.BsrMatrix, block_shape, coarse=None):
+ coarse_type = wp.mat(shape=block_shape, dtype=src.scalar_type)
+ block_ratios = (
+ block_shape[0] // src.block_shape[0],
+ block_shape[1] // src.block_shape[1],
+ )
+
+ if coarse is None:
+ coarse = sp.bsr_zeros(
+ rows_of_blocks=src.nrow // block_ratios[0],
+ cols_of_blocks=src.ncol // block_ratios[1],
+ block_type=coarse_type,
+ )
+ else:
+ sp.bsr_set_zero(
+ coarse,
+ rows_of_blocks=src.nrow // block_ratios[0],
+ cols_of_blocks=src.ncol // block_ratios[1],
+ )
+
+ # compute the structure
+ nnz = src.nnz_sync() // (block_ratios[0] * block_ratios[1])
+ if coarse.columns.shape[0] < nnz:
+ coarse.columns = wp.empty(nnz, dtype=int)
+ if coarse.values.shape[0] < nnz:
+ coarse.values = wp.empty(nnz, dtype=coarse_type)
+ wp.launch(
+ _coarsen_structure,
+ dim=coarse.nrow,
+ inputs=[
+ block_ratios[0],
+ block_ratios[1],
+ src.offsets,
+ src.columns,
+ coarse.offsets,
+ coarse.columns,
+ ],
+ )
+ coarse.nnz = nnz
+ coarse.copy_nnz_async()
+ sp.bsr_assign(src=src, dest=coarse, masked=True)
+
+ return coarse
+
+ # TODO for later
+ # masked always keep structure
+ # bsr_prune to remove actual zeros?
+
+
+@wp.kernel
+def bsr_mul_diag(
+ Bt_values: wp.array3d(dtype=float),
+ Bt_columns: wp.array(dtype=int),
+ C_values: wp.array(dtype=Any),
+):
+ i, r = wp.tid()
+ col = Bt_columns[i]
+
+ C = C_values[col]
+
+ Btr = Bt_values[i, r]
+ BtC = wp.vec3(Btr[0], Btr[1], Btr[2]) @ C
+ for k in range(3):
+ Btr[k] = BtC[k]
diff --git a/deps/vomp/vomp/fem/fem_examples/simplicits/qp_basis_space.py b/deps/vomp/vomp/fem/fem_examples/simplicits/qp_basis_space.py
new file mode 100644
index 0000000000000000000000000000000000000000..20f9e94c69828e38b98760e3a56cc5fb37bb9932
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/simplicits/qp_basis_space.py
@@ -0,0 +1,508 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Dict, Any
+
+import warp as wp
+import warp.fem as fem
+
+
+class ProductShapeFunction(fem.space.shape.CubeShapeFunction):
+ """Shape function defined as the product of two shapes
+ N(x) = sum_i sum_j N0_i(x) N1_j(x)
+
+ N0 is used to define the node topology (connectivity between cells),
+ while N1 nodes are duplicated at each N0 node
+ """
+
+ def __init__(
+ self,
+ shape0: fem.space.ShapeFunction,
+ shape1: fem.space.ShapeFunction,
+ shape1_duplicates: int = 1,
+ ):
+ self._shape0 = shape0
+ self._shape1 = shape1
+ self._duplicates = shape1_duplicates
+
+ assert shape0.value == fem.space.ShapeFunction.Value.Scalar
+
+ self.ORDER = self._shape0.ORDER * self._shape1.ORDER
+ duplicated_shape1_nodes = self._duplicates * self._shape1.NODES_PER_ELEMENT
+
+ self.NODES_PER_ELEMENT = (
+ self._shape0.NODES_PER_ELEMENT * duplicated_shape1_nodes
+ )
+
+ if isinstance(shape0, fem.space.shape.CubeShapeFunction):
+ self.VERTEX_NODE_COUNT = (
+ self._shape0.VERTEX_NODE_COUNT * duplicated_shape1_nodes
+ )
+ self.EDGE_NODE_COUNT = (
+ self._shape0.EDGE_NODE_COUNT * duplicated_shape1_nodes
+ )
+ self.FACE_NODE_COUNT = (
+ self._shape0.FACE_NODE_COUNT * duplicated_shape1_nodes
+ )
+ self.INTERIOR_NODE_COUNT = (
+ self._shape0.INTERIOR_NODE_COUNT * duplicated_shape1_nodes
+ )
+
+ self.node_type_and_type_index = self._get_node_type_and_type_index()
+ self.split_node_indices = self._get_split_node_indices()
+
+ @property
+ def value(self) -> fem.space.ShapeFunction.Value:
+ return self._shape1.value
+
+ @property
+ def name(self):
+ return f"{self._shape0.name}x{self._shape1.name}x{self._duplicates}"
+
+ def _get_split_node_indices(self):
+ N1_DUPLICATED_NODES = self._duplicates * self._shape1.NODES_PER_ELEMENT
+ N_DUPLICATES = self._duplicates
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def split_node_indices(
+ node_index_in_elt: int,
+ ):
+ n0_index = node_index_in_elt // N1_DUPLICATED_NODES
+ n1_dup_index = node_index_in_elt - N1_DUPLICATED_NODES * n0_index
+ n1_index = n1_dup_index // N_DUPLICATES
+
+ return n0_index, n1_index
+
+ return split_node_indices
+
+ def _get_node_type_and_type_index(self):
+ N1_DUPLICATED_NODES = self._duplicates * self._shape1.NODES_PER_ELEMENT
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def node_type_and_index(
+ node_index_in_elt: int,
+ ):
+ n0_index = node_index_in_elt // N1_DUPLICATED_NODES
+ n1_dup_index = node_index_in_elt - N1_DUPLICATED_NODES * n0_index
+ node_type, type_instance, type_index = (
+ self._shape0.node_type_and_type_index(n0_index)
+ )
+
+ return (
+ node_type,
+ type_instance,
+ (type_index * N1_DUPLICATED_NODES + n1_dup_index),
+ )
+
+ return node_type_and_index
+
+ def make_element_inner_weight(self):
+ n0_weight = self._shape0.make_element_inner_weight()
+ n0_coords = self._shape0.make_node_coords_in_element()
+ n1_weight = self._shape1.make_element_inner_weight()
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def element_inner_weight(
+ coords: fem.Coords,
+ node_index_in_elt: int,
+ ):
+ n0_index, n1_index = self.split_node_indices(node_index_in_elt)
+ n1_coords = coords + wp.vec3(0.5) - n0_coords(n0_index)
+
+ clamped_coords = wp.min(wp.max(coords, wp.vec3(0.0)), wp.vec3(1.0))
+
+ return n0_weight(clamped_coords, n0_index) * n1_weight(n1_coords, n1_index)
+
+ return element_inner_weight
+
+ def make_element_inner_weight_gradient(self):
+ n0_weight = self._shape0.make_element_inner_weight()
+ n1_weight = self._shape1.make_element_inner_weight()
+ n0_grad = self._shape0.make_element_inner_weight_gradient()
+ n1_grad = self._shape1.make_element_inner_weight_gradient()
+ n0_coords = self._shape0.make_node_coords_in_element()
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def element_inner_weight_gradient(
+ coords: fem.Coords,
+ node_index_in_elt: int,
+ ):
+ n0_index, n1_index = self.split_node_indices(node_index_in_elt)
+ n1_coords = coords + wp.vec3(0.5) - n0_coords(n0_index)
+
+ clamped_coords = wp.min(wp.max(coords, wp.vec3(0.0)), wp.vec3(1.0))
+ n0_grad_clamped = n0_grad(clamped_coords, n0_index)
+ # Fix gradient for out-of cell coordinates
+ for k in range(3):
+ if coords[k] < 0 or coords[k] > 1:
+ n0_grad_clamped[k] = 0.0
+
+ return n0_weight(clamped_coords, n0_index) * n1_grad(
+ n1_coords, n1_index
+ ) + n0_grad_clamped * n1_weight(n1_coords, n1_index)
+
+ return element_inner_weight_gradient
+
+ # boilerplate for nodal integration
+ # (not used here)
+
+ def make_node_coords_in_element(self):
+ N1_DUPLICATED_NODES = self._shape1.NODES_PER_ELEMENT * self._duplicates
+ n0_coords = self._shape0.make_node_coords_in_element()
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def node_coords_in_element(
+ node_index_in_elt: int,
+ ):
+ n0_index = node_index_in_elt // N1_DUPLICATED_NODES
+ return n0_coords(n0_index)
+
+ return node_coords_in_element
+
+ def make_node_quadrature_weight(self):
+ N1_DUPLICATED_NODES = self._shape1.NODES_PER_ELEMENT * self._duplicates
+ n0_weight = self._shape0.make_node_quadrature_weight()
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def node_quadrature_weight(node_index_in_element: int):
+ n0_index = node_index_in_element // N1_DUPLICATED_NODES
+ return n0_weight(n0_index) / float(N1_DUPLICATED_NODES)
+
+ return node_quadrature_weight
+
+ def make_trace_node_quadrature_weight(self):
+ N1_DUPLICATED_NODES = self._shape1.NODES_PER_ELEMENT * self._duplicates
+ n0_trace_weight = self._shape0.make_trace_node_quadrature_weight()
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def trace_node_quadrature_weight(node_index_in_element: int):
+ n0_index = node_index_in_element // N1_DUPLICATED_NODES
+ return n0_trace_weight(n0_index) / float(N1_DUPLICATED_NODES)
+
+ return trace_node_quadrature_weight
+
+
+class SmoothStepShapeFunction(fem.space.shape.CubeTripolynomialShapeFunctions):
+ """Like trilinear, but with smooth step instead"""
+
+ def __init__(
+ self,
+ ):
+ super().__init__(degree=1, family=fem.Polynomial.LOBATTO_GAUSS_LEGENDRE)
+
+ @property
+ def name(self):
+ return "SmoothStep"
+
+ @wp.func
+ def _smoothstep(x: float):
+ t = 1.0 - wp.abs(x)
+ # t2 = t * t
+ # return 3.0 * t2 - 2.0 * t * t2
+ # t = wp.abs(x)
+ t3 = t * t * t
+ return t3 * (t * (t * 6.0 - 15.0) + 10.0)
+
+ @wp.func
+ def _smoothstep_grad(x: float):
+ t = wp.abs(x)
+ # t2 = t * t
+ # g = 6.0 * (t - t2)
+
+ g = t * t * (3.0 * (t * (t * 6.0 - 15.0) + 10.0) + t * (12.0 * t - 15.0))
+
+ return -wp.sign(x) * g
+
+ def make_element_inner_weight(self):
+ @fem.cache.dynamic_func(suffix=self.name)
+ def element_inner_weight(
+ coords: fem.Coords,
+ node_index_in_elt: int,
+ ):
+ v = self._vertex_coords_f(node_index_in_elt)
+ off = coords - v
+
+ wx = SmoothStepShapeFunction._smoothstep(off[0])
+ wy = SmoothStepShapeFunction._smoothstep(off[1])
+ wz = SmoothStepShapeFunction._smoothstep(off[2])
+
+ return wx * wy * wz
+
+ return element_inner_weight
+
+ def make_element_inner_weight_gradient(self):
+ @fem.cache.dynamic_func(suffix=self.name)
+ def element_inner_weight_gradient(
+ coords: fem.Coords,
+ node_index_in_elt: int,
+ ):
+ v = self._vertex_coords_f(node_index_in_elt)
+ off = coords - v
+
+ wx = SmoothStepShapeFunction._smoothstep(off[0])
+ wy = SmoothStepShapeFunction._smoothstep(off[1])
+ wz = SmoothStepShapeFunction._smoothstep(off[2])
+
+ dx = SmoothStepShapeFunction._smoothstep_grad(off[0])
+ dy = SmoothStepShapeFunction._smoothstep_grad(off[1])
+ dz = SmoothStepShapeFunction._smoothstep_grad(off[2])
+
+ return wp.vec3(dx * wy * wz, dy * wz * wx, dz * wx * wy)
+
+ return element_inner_weight_gradient
+
+
+class DuplicatedBasisSpace(fem.BasisSpace):
+ """
+ Basis space with duplicated nodes weighted by shape functions
+ presampled at each quadrature point
+ """
+
+ @wp.struct
+ class BasisArg:
+ weights: wp.array3d(dtype=float)
+ weight_gradients: wp.array3d(dtype=wp.vec3)
+ subset_indices: wp.array(dtype=int)
+
+ def __init__(
+ self,
+ topology: fem.SpaceTopology,
+ shape: ProductShapeFunction,
+ duplicate_count: int,
+ ):
+ super().__init__(topology)
+ self._shape = shape
+ self._duplicate_count = duplicate_count
+
+ self._weights = None
+ self._weight_gradients = None
+ self._subset_indices = None
+
+ self.ORDER = self._shape.ORDER
+
+ @property
+ def value(self) -> fem.space.shape.ShapeFunction.Value:
+ return self._shape.value
+
+ @property
+ def name(self):
+ return f"{self.topology.name}_{self._shape.name}"
+
+ def basis_arg_value(self, device):
+ args = self.BasisArg()
+ args.weights = self._weights.to(device)
+ args.weight_gradients = self._weight_gradients.to(device)
+ args.subset_indices = (
+ None if self._subset_indices is None else self._subset_indices.to(device)
+ )
+
+ return args
+
+ def set_cached_qp_weights_and_gradients(
+ self, weights: wp.array, weight_gradients: wp.array
+ ):
+ if not weights:
+ weights = self._weights
+ if not weight_gradients:
+ weight_gradients = self._weight_gradients
+
+ self._weights, prev_weights = weights, self._weights
+ self._weight_gradients, prev_gradients = (
+ weight_gradients,
+ self._weight_gradients,
+ )
+ return prev_weights, prev_gradients
+
+ def set_subset_indices(self, subset_indices: wp.array):
+ self._subset_indices, prev_indices = subset_indices, self._subset_indices
+ return prev_indices
+
+ def make_element_inner_weight(self):
+ shape_element_inner_weight = self._shape.make_element_inner_weight()
+ DUPLICATE_COUNT = self._duplicate_count
+ VERTEX_NODE_COUNT = DUPLICATE_COUNT * self._shape._shape1.NODES_PER_ELEMENT
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def element_inner_weight(
+ elt_arg: self.geometry.CellArg,
+ basis_arg: self.BasisArg,
+ element_index: fem.ElementIndex,
+ coords: fem.Coords,
+ node_index_in_elt: int,
+ qp_index: fem.QuadraturePointIndex,
+ ):
+ duplicate_idx = node_index_in_elt % DUPLICATE_COUNT
+
+ vertex_idx = node_index_in_elt // VERTEX_NODE_COUNT
+
+ if basis_arg.subset_indices.shape[0] > 0:
+ qp_index = basis_arg.subset_indices[qp_index]
+
+ return basis_arg.weights[
+ qp_index, vertex_idx, duplicate_idx
+ ] * shape_element_inner_weight(coords, node_index_in_elt)
+
+ return element_inner_weight
+
+ def make_element_inner_weight_gradient(self):
+ shape_element_inner_weight_gradient = (
+ self._shape.make_element_inner_weight_gradient()
+ )
+ shape_element_inner_weight = self._shape.make_element_inner_weight()
+ DUPLICATE_COUNT = self._duplicate_count
+ VERTEX_NODE_COUNT = DUPLICATE_COUNT * self._shape._shape1.NODES_PER_ELEMENT
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def element_inner_weight_gradient(
+ elt_arg: self.geometry.CellArg,
+ basis_arg: self.BasisArg,
+ element_index: fem.ElementIndex,
+ coords: fem.Coords,
+ node_index_in_elt: int,
+ qp_index: fem.QuadraturePointIndex,
+ ):
+ duplicate_idx = node_index_in_elt % DUPLICATE_COUNT
+
+ vertex_idx = node_index_in_elt // VERTEX_NODE_COUNT
+
+ if basis_arg.subset_indices.shape[0] > 0:
+ qp_index = basis_arg.subset_indices[qp_index]
+
+ return basis_arg.weights[
+ qp_index, vertex_idx, duplicate_idx
+ ] * shape_element_inner_weight_gradient(
+ coords, node_index_in_elt
+ ) + basis_arg.weight_gradients[
+ qp_index, vertex_idx, duplicate_idx
+ ] * shape_element_inner_weight(
+ coords, node_index_in_elt
+ )
+
+ return element_inner_weight_gradient
+
+ # Disable nodal integration
+
+ def make_node_coords_in_element(self):
+ return None
+
+ def make_node_quadrature_weight(self):
+ return None
+
+ def make_trace_node_quadrature_weight(self, trace_basis):
+ return None
+
+
+class QPBasedImplicitField(fem.field.GeometryField):
+ """Same as fem.ImplicitField, but passes QP index instead of grid-space position"""
+
+ def __init__(
+ self,
+ domain: fem.GeometryDomain,
+ func: wp.Function,
+ values: Optional[Dict[str, Any]] = None,
+ degree=0,
+ ):
+ self.domain = domain
+ self._degree = degree
+
+ if not isinstance(func, wp.Function):
+ raise ValueError(
+ "Implicit field function must be a warp Function (decorated with `wp.func`)"
+ )
+
+ self._func = func
+
+ argspec = fem.integrand(func.func).argspec
+ arg_types = argspec.annotations
+
+ pos_arg_type = arg_types.pop(argspec.args[0]) if arg_types else None
+ if not pos_arg_type or not wp.types.types_equal(
+ pos_arg_type,
+ int,
+ match_generic=True,
+ ):
+ raise ValueError(
+ f"QP-based Implicit field function '{func.func.__name__}' must accept an index as its first argument"
+ )
+
+ self.EvalArg = fem.cache.get_argument_struct(arg_types)
+ self.values = values
+
+ self.ElementEvalArg = self._make_element_eval_arg()
+ self.eval_degree = self._make_eval_degree()
+
+ self.eval_inner = self._make_eval_func(func)
+ self.eval_outer = self.eval_inner
+
+ @property
+ def values(self):
+ return self._func_arg
+
+ @values.setter
+ def values(self, v):
+ self._func_arg = fem.cache.populate_argument_struct(
+ self.EvalArg, v, self._func.func.__name__
+ )
+
+ @property
+ def geometry(self):
+ return self.domain.geometry
+
+ @property
+ def element_kind(self):
+ return self.domain.element_kind
+
+ def eval_arg_value(self, device):
+ return self._func_arg
+
+ @property
+ def degree(self) -> int:
+ return self._degree
+
+ @property
+ def name(self) -> str:
+ return f"Implicit_{self.domain.name}_{self.degree}_{self.EvalArg.key}"
+
+ def _make_eval_func(self, func):
+ if func is None:
+ return None
+
+ @fem.cache.dynamic_func(
+ suffix=f"{self.name}_{func.key}",
+ code_transformers=[
+ fem.cache.ExpandStarredArgumentStruct({"args.eval_arg": self.EvalArg})
+ ],
+ )
+ def eval_inner(args: self.ElementEvalArg, s: fem.Sample):
+ return func(s.qp_index, *args.eval_arg)
+
+ return eval_inner
+
+ def _make_element_eval_arg(self):
+ @fem.cache.dynamic_struct(suffix=self.name)
+ class ElementEvalArg:
+ elt_arg: self.domain.ElementArg
+ eval_arg: self.EvalArg
+
+ return ElementEvalArg
+
+ def _make_eval_degree(self):
+ ORDER = wp.constant(self._degree)
+
+ @fem.cache.dynamic_func(suffix=self.name)
+ def degree(args: self.ElementEvalArg):
+ return ORDER
+
+ return degree
diff --git a/deps/vomp/vomp/fem/fem_examples/simplicits/sparse_blended_sim.py b/deps/vomp/vomp/fem/fem_examples/simplicits/sparse_blended_sim.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3595fb7962913db3f82e36a8348fd72781918b7
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/simplicits/sparse_blended_sim.py
@@ -0,0 +1,1853 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import NamedTuple, Optional, Callable, List, Dict, Any
+import gc
+
+import numpy as np
+import warp as wp
+import warp.fem as fem
+import warp.sparse as sp
+from warp.optim.linear import LinearOperator
+
+import argparse
+import polyscope as ps
+
+from fem_examples.mfem.softbody_sim import ClassicFEM, defgrad
+from fem_examples.simplicits.qp_basis_space import (
+ DuplicatedBasisSpace,
+ QPBasedImplicitField,
+ ProductShapeFunction,
+ SmoothStepShapeFunction,
+)
+from fem_examples.simplicits.collisions import (
+ detect_ground_collisions,
+ detect_particle_collisions,
+ detect_mesh_collisions,
+ compute_collision_bounds,
+ collision_energy,
+ collision_gradient_and_hessian,
+ apply_collision_bounds,
+)
+from fem_examples.simplicits.linalg import (
+ bsr_coarsen_aligned,
+ bsr_mul_diag,
+ create_batched_cholesky_kernel,
+ create_batched_cholesky_solve_kernel,
+ dense_chol_batched,
+ dense_chol_subs_batched,
+)
+
+
+import warp.examples.fem.utils as fem_example_utils
+
+
+class SparseBlendedSim(ClassicFEM):
+ def __init__(
+ self, geo: fem.Geometry, active_cells: wp.array, n_duplicates: int, args
+ ):
+ self._n_duplicates = n_duplicates
+
+ self.warp_meshes = []
+
+ self._prescribed_pos_weight_field = None
+ self._prescribed_pos_field = None
+ self._tiled_lhs = None
+
+ super().__init__(geo, active_cells, args)
+
+ @property
+ def domain(self):
+ return self.u_test.domain
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ ClassicFEM.add_parser_arguments(parser)
+ parser.add_argument(
+ "--weight_degree",
+ type=int,
+ default=1,
+ help="Degree of grid polynomial basis function (1 means trilinear, 2 triquadratic, etc)",
+ )
+ parser.add_argument(
+ "--collision_stiffness",
+ "-ck",
+ type=float,
+ default=1.0e3,
+ help="Multiplier for collision force/energy",
+ )
+ parser.add_argument(
+ "--collision_radius",
+ "-cr",
+ type=float,
+ default=0.1,
+ help="Radius of interaction for collision particles",
+ )
+ parser.add_argument(
+ "--collision_detection_ratio",
+ "-cd",
+ type=float,
+ default=1.25,
+ help="Multiplier of collision radius for detection",
+ )
+ parser.add_argument(
+ "--collision_barrier_ratio",
+ "-cp",
+ type=float,
+ default=0.5,
+ help="Fraction of collision radius that is non-penetrable",
+ )
+ parser.add_argument(
+ "--self_immunity_radius_ratio",
+ "-cs",
+ type=float,
+ default=4.0,
+ help="Ignore self-collision for particles that were within this ratio at rest",
+ )
+ parser.add_argument("--friction", "-mu", type=float, default=0.2)
+ parser.add_argument(
+ "--friction_reg",
+ "-mur",
+ type=float,
+ default=0.1,
+ help="Regularization coefficient for IPC friction",
+ )
+ parser.add_argument(
+ "--friction_fluid",
+ "-nu",
+ type=float,
+ default=0.01,
+ help="Additional fluid friction ratio to convexify things",
+ )
+ parser.add_argument(
+ "--bounds",
+ action=argparse.BooleanOptionalAction,
+ help="Enforce non-penetration collision bounds",
+ )
+ parser.add_argument(
+ "--admm_iterations",
+ "-admm",
+ type=int,
+ default=0,
+ help="Use ADMM solver instead of IPC. Discouraged.",
+ )
+ parser.add_argument(
+ "--dual-grid",
+ action=argparse.BooleanOptionalAction,
+ default=False,
+ help="Force running on primal grid instead of dual grid",
+ )
+ parser.add_argument(
+ "--smoothstep",
+ action=argparse.BooleanOptionalAction,
+ help="Use tri-smoothstep grid functions instead of tri-polynomial",
+ )
+ parser.add_argument(
+ "--precond_reg",
+ "-preg",
+ type=float,
+ default=0.0001,
+ help="preconditioner regularization",
+ )
+ parser.add_argument(
+ "--direct",
+ action=argparse.BooleanOptionalAction,
+ help="Use (dense) direct solver for newton systems",
+ )
+ parser.add_argument(
+ "--tiles",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Use tile-based kernels. Requires recent warp built with libmathdx",
+ )
+ parser.add_argument(
+ "--off-diagonal",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Include off-diagonal terms in contact matrix",
+ )
+ parser.add_argument(
+ "--ground",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Do ground collisions",
+ )
+ parser.add_argument(
+ "--ground_height",
+ type=float,
+ default=0.0,
+ help="Ground height",
+ )
+
+ def set_quadrature(
+ self,
+ cell_indices,
+ cell_coords,
+ measures: wp.array,
+ qp_obj_ids: wp.array,
+ qp_stiffness_scales: wp.array,
+ qp_node_weights: wp.array = None,
+ qp_node_gradients: wp.array = None,
+ ):
+ self.cell_indices = cell_indices
+ self.cell_coords = cell_coords
+
+ quadrature = fem.PicQuadrature(
+ domain=self.vel_quadrature.domain,
+ positions=(self.cell_indices, self.cell_coords),
+ measures=measures,
+ )
+
+ self.set_cached_basis_qp_weights(qp_node_weights, qp_node_gradients)
+
+ self.vel_quadrature = quadrature
+ self.elasticity_quadrature = quadrature
+
+ self.qp_obj_ids = qp_obj_ids
+ self.qp_stiff_scale = qp_stiffness_scales
+
+ self.lame_field = QPBasedImplicitField(
+ quadrature.domain,
+ _lame_field,
+ values={
+ "lame_ref": self.lame_ref,
+ "qp_stiff_scale": self.qp_stiff_scale,
+ },
+ )
+
+ def set_cached_basis_qp_weights(
+ self,
+ qp_node_weights: wp.array = None,
+ qp_node_gradients: wp.array = None,
+ ):
+ if isinstance(self._vel_basis, DuplicatedBasisSpace):
+ return self._vel_basis.set_cached_qp_weights_and_gradients(
+ qp_node_weights, qp_node_gradients
+ )
+
+ return None, None
+
+ def _init_displacement_basis(self):
+ # weights
+
+ if self.args.smoothstep:
+ assert self.args.weight_degree == 1
+ shape0 = SmoothStepShapeFunction()
+ else:
+ shape0 = fem.space.shape.get_shape_function(
+ self.geo.reference_cell(),
+ self.geo.dimension,
+ degree=self.args.weight_degree,
+ element_basis=fem.ElementBasis.LAGRANGE,
+ )
+
+ # handles
+ shape1 = fem.space.shape.get_shape_function(
+ self.geo.reference_cell(),
+ self.geo.dimension,
+ self.args.degree,
+ fem.ElementBasis.NONCONFORMING_POLYNOMIAL,
+ )
+ self._handle_size = shape1.NODES_PER_ELEMENT
+
+ shape = ProductShapeFunction(
+ shape0, shape1, shape1_duplicates=self._n_duplicates
+ )
+
+ if self.args.weight_degree == 0:
+ topology = fem.space.topology.RegularDiscontinuousSpaceTopology(
+ self.geo, shape.NODES_PER_ELEMENT
+ )
+ elif isinstance(self.geo.base, fem.Grid3D):
+ topology = fem.space.make_grid_3d_space_topology(self.geo, shape)
+ else:
+ topology = fem.space.make_hexmesh_space_topology(self.geo, shape)
+
+ if self._n_duplicates > 1:
+ basis_space = DuplicatedBasisSpace(
+ topology, shape, duplicate_count=self._n_duplicates
+ )
+ else:
+ basis_space = fem.space.ShapeBasisSpace(topology, shape)
+
+ self.set_displacement_basis(basis_space)
+
+ self._use_tiles = self.args.tiles
+ if self._use_tiles:
+ self._tile_size = self._handle_size * 3 * self._n_duplicates
+ self._use_tiles = self._tile_size > 6 and self._tile_size < 90
+
+ def compute_initial_guess(self):
+ self.du_field.dof_values.zero_()
+ self.detect_collisions()
+
+ def set_prescribed_positions(self, pos_field, weight_field):
+ # for driving objects kinematically
+ self._prescribed_pos_field = pos_field
+ self._prescribed_pos_weight_field = weight_field
+
+ def evaluate_energy(self):
+ E_p, c_r = super().evaluate_energy()
+
+ if self._prescribed_pos_field:
+ E_p += fem.integrate(
+ prescribed_position_energy_form,
+ quadrature=self.vel_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "stiffness": self._prescribed_pos_weight_field,
+ "target": self._prescribed_pos_field,
+ },
+ )
+
+ if self.n_contact > 0:
+ self._sample_cp_displacement(self.du_field, dest=self.cp_du)
+ col_energies = wp.empty(self.n_contact, dtype=float)
+ wp.launch(
+ collision_energy,
+ dim=self.n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.collision_barrier_ratio,
+ self.args.friction,
+ self.args.dt * self.args.friction_reg,
+ self.args.friction_fluid * self.args.friction_reg,
+ self.cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ col_energies,
+ ],
+ )
+ E_col = self._collision_stiffness * wp.utils.array_sum(col_energies)
+
+ E_p += E_col
+
+ return E_p, c_r
+
+ def newton_lhs(self):
+ lhs = super().newton_lhs()
+
+ if self._prescribed_pos_field:
+ z = fem.integrate(
+ prescribed_position_lhs_form,
+ quadrature=self.vel_quadrature,
+ fields={
+ "u": self.u_trial,
+ "v": self.u_test,
+ "stiffness": self._prescribed_pos_weight_field,
+ },
+ output_dtype=float,
+ )
+ lhs += z
+
+ # coarsen if requested
+ if self._use_tiles:
+ lhs = bsr_coarsen_aligned(
+ lhs,
+ block_shape=(self._tile_size, self._tile_size),
+ coarse=self._tiled_lhs,
+ )
+ self._tiled_lhs = lhs
+
+ # contacts
+ if self.n_contact > 0 and not self.args.admm_iterations:
+ H = self._collision_jacobian
+ Ht = self._collision_jacobian_t
+ wp.launch(
+ bsr_mul_diag,
+ dim=(Ht.nnz, Ht.block_shape[0]),
+ inputs=[Ht.scalar_values, Ht.columns, self._col_energy_hessian],
+ )
+
+ sp.bsr_mm(
+ x=Ht,
+ y=H,
+ z=lhs,
+ alpha=self._collision_stiffness,
+ beta=1.0,
+ masked=not self.args.off_diagonal,
+ work_arrays=self._HtH_work_arrays,
+ )
+
+ return lhs
+
+ def newton_rhs(self, tape=None):
+ rhs = super().newton_rhs(tape)
+
+ if self._prescribed_pos_field:
+ fem.integrate(
+ prescribed_position_rhs_form,
+ quadrature=self.vel_quadrature,
+ fields={
+ "u_cur": self.u_field,
+ "v": self.u_test,
+ "stiffness": self._prescribed_pos_weight_field,
+ "target": self._prescribed_pos_field,
+ },
+ output=rhs,
+ add=True,
+ )
+
+ # contacts
+ if self.n_contact > 0 and not self.args.admm_iterations:
+ sp.bsr_mv(
+ A=self._collision_jacobian_t,
+ x=self._col_energy_gradients,
+ y=rhs,
+ alpha=-self._collision_stiffness,
+ beta=1.0,
+ )
+
+ return rhs
+
+ def solve_newton_system(self, lhs, rhs):
+ # from warp.tests.test_sparse import _bsr_to_dense
+ # A = _bsr_to_dense(lhs)
+ # print("COND::", np.linalg.cond(A))
+
+ gc.collect(0)
+
+ if self.n_contact > 0 and self.args.admm_iterations > 0:
+ return self._solve_admm(lhs, rhs)
+
+ if self.args.direct:
+ # those imports should not be there
+ # but ideally this file would be torch-free...
+ import torch
+ from simplicits import bsr_to_torch
+
+ lhs = bsr_to_torch(lhs).to_dense()
+ rhs = wp.to_torch(rhs).flatten()
+ res = torch.linalg.solve(lhs, rhs)
+ res = wp.from_torch(res.reshape(-1, 3), dtype=wp.vec3)
+ else:
+ # return super().solve_newton_system(lhs, rhs)
+
+ res = wp.zeros_like(rhs)
+
+ gc.disable()
+
+ P = self._build_preconditioner(lhs)
+
+ fem_example_utils.bsr_cg(
+ A=lhs,
+ b=rhs,
+ x=res,
+ quiet=True,
+ M=P,
+ use_diag_precond=False,
+ tol=self.args.cg_tol,
+ max_iters=self.args.cg_iters,
+ )
+
+ gc.enable()
+
+ self._dof_bounds = wp.ones(res.shape, dtype=float)
+ if self.n_contact > 0 and self.args.bounds:
+ self._compute_bounds(res)
+
+ return (res,)
+
+ def _compute_bounds(self, delta_du: wp.array):
+ # compute position increment for each quadrature point
+ delta_du_field = fem.make_discrete_field(
+ space=self.u_field.space, space_partition=self.u_field.space_partition
+ )
+ delta_du_field.dof_values = delta_du
+ cp_delta_du = wp.empty_like(self.cp_du)
+ self._sample_cp_displacement(delta_du_field, dest=cp_delta_du)
+
+ # delta_gap = self._collision_jacobian @ delta_du
+
+ wp.launch(
+ compute_collision_bounds,
+ dim=self.n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.collision_barrier_ratio,
+ self.cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ cp_delta_du,
+ self._collision_jacobian_a.offsets,
+ self._collision_jacobian_a.columns,
+ self._collision_jacobian_b.offsets,
+ self._collision_jacobian_b.columns,
+ self._dof_bounds,
+ ],
+ )
+
+ def apply_newton_deltas(self, delta_fields, alpha=1.0):
+ # Restore checkpoint
+ wp.copy(src=self._u_cur, dest=self.u_field.dof_values)
+ wp.copy(src=self._du_cur, dest=self.du_field.dof_values)
+
+ delta_du = delta_fields[0]
+
+ wp.launch(
+ apply_collision_bounds,
+ dim=delta_du.shape,
+ inputs=[
+ delta_du,
+ alpha,
+ self._dof_bounds,
+ self.u_field.dof_values,
+ self.du_field.dof_values,
+ ],
+ )
+
+ def _solve_admm(self, lhs, rhs):
+ k = self._collision_stiffness
+ H = self._collision_jacobian
+ Ht = self._collision_jacobian.transpose()
+ n_contact = self.n_contact
+
+ lhs += k * (Ht @ H)
+ P = self._build_preconditioner(lhs)
+
+ gap_zero = self.collision_kinematic_gaps[:n_contact]
+ sp.bsr_mv(A=H, x=self.du_field.dof_values, y=gap_zero, alpha=1.0, beta=1.0)
+ gap = wp.clone(gap_zero)
+
+ tau = wp.clone(gap)
+ lbd = wp.zeros_like(gap_zero)
+
+ rhs_i = wp.zeros_like(rhs)
+ ddu = wp.zeros_like(rhs)
+
+ for it in range(self.args.admm_iterations):
+ # rhs_i = rhs + k Ht( gap - v + ldb)
+
+ wp.copy(src=rhs, dest=rhs_i)
+ sp.bsr_mv(A=Ht, x=gap, y=rhs_i, alpha=k, beta=1.0)
+ sp.bsr_mv(A=Ht, x=tau, y=rhs_i, alpha=-k, beta=1.0)
+ sp.bsr_mv(A=Ht, x=lbd, y=rhs_i, alpha=k, beta=1.0)
+
+ fem_example_utils.bsr_cg(
+ A=lhs,
+ b=rhs_i,
+ x=ddu,
+ quiet=True,
+ M=P,
+ tol=self.args.cg_tol,
+ max_iters=self.args.cg_iters,
+ )
+
+ # update gap = H ddu + gap_0
+ wp.copy(src=gap_zero, dest=gap)
+ sp.bsr_mv(A=H, x=ddu, y=gap, alpha=1.0, beta=1.0)
+
+ # project on contacts
+ wp.launch(
+ SparseBlendedSim._solve_admm_contacts,
+ dim=n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.collision_barrier_ratio,
+ self.args.friction,
+ self.collision_normals,
+ gap_zero,
+ gap,
+ tau,
+ lbd,
+ ],
+ )
+
+ print(it, np.linalg.norm(gap.numpy() - tau.numpy()))
+
+ if self.args.bounds:
+ self._enforce_bounds(ddu)
+
+ return (ddu,)
+
+ @wp.kernel
+ def _solve_admm_contacts(
+ rc: float,
+ rp_ratio: float,
+ mu: float,
+ normals: wp.array(dtype=wp.vec3),
+ gaps0: wp.array(dtype=wp.vec3),
+ gaps: wp.array(dtype=wp.vec3),
+ taus: wp.array(dtype=wp.vec3),
+ lbds: wp.array(dtype=wp.vec3),
+ ):
+ c = wp.tid()
+ nor = normals[c]
+
+ # rp = wp.min(rc * rp_ratio, wp.max(0.0, wp.dot(gaps0[c], nor)))
+ rp = rc * rp_ratio
+
+ gap = gaps[c] - rp * nor
+ guess = gap - lbds[c]
+
+ # solve coulomb
+ un = wp.dot(guess, nor)
+ if un < 0.0:
+ guess -= un * nor
+ uTn = wp.length_sq(guess)
+ alpha = mu * un
+ if uTn <= alpha * alpha:
+ guess = wp.vec3(0.0)
+ else:
+ guess *= 1.0 + mu * un / wp.sqrt(uTn)
+
+ taus[c] = guess + rp * nor
+ lbds[c] += guess - gap
+
+ def _build_preconditioner(self, lhs):
+ typical_inertia = self.typical_length**3 * self.args.density / self.args.dt**2
+ p_reg = self.args.precond_reg * typical_inertia
+ if p_reg < 0:
+ # disable preconditioner
+ return None
+
+ # Block-diagonal preconditioner with handle-sized blocks (12x12)
+ use_tiles = self._use_tiles
+ if use_tiles:
+ block_size = self._tile_size
+ else:
+ block_size = self._handle_size * 3
+
+ block_type = wp.mat((block_size, block_size), dtype=float)
+
+ P_coarse = sp.bsr_diag(
+ rows_of_blocks=lhs.shape[0] // block_size,
+ block_type=block_type,
+ )
+ sp.bsr_assign(src=lhs, dest=P_coarse, masked=True)
+
+ if use_tiles:
+
+ def _as_float_array(x):
+ return wp.array(
+ ptr=x.ptr, shape=(x.shape[0] * 3), device=x.device, dtype=float
+ )
+
+ has_tile_chol = "tile_cholesky" in wp.context.builtin_functions
+
+ if not has_tile_chol:
+ P_values = P_coarse.scalar_values.reshape(
+ (P_coarse.nrow, block_size * block_size)
+ )
+ L = wp.empty_like(P_values)
+
+ wp.launch(
+ dense_chol_batched,
+ dim=(P_coarse.nrow),
+ inputs=[block_size, p_reg, P_values, L],
+ )
+
+ def P_inv_mv(x, y, z, alpha, beta):
+ # for cg, y = z, alpha = 1, beta = 0
+ x = _as_float_array(x).reshape((P_coarse.nrow, block_size))
+ z = _as_float_array(z).reshape((P_coarse.nrow, block_size))
+ wp.launch(
+ dense_chol_subs_batched,
+ dim=(P_coarse.nrow),
+ inputs=[x, z, L],
+ )
+
+ return LinearOperator(
+ P_coarse.shape, P_coarse.dtype, P_coarse.device, P_inv_mv
+ )
+
+ P_values = P_coarse.scalar_values
+ tile_chol = create_batched_cholesky_kernel(block_size)
+ tile_solve = create_batched_cholesky_solve_kernel(block_size)
+
+ BLOCK_DIM = 128
+
+ wp.launch(
+ tile_chol,
+ dim=(P_coarse.nrow, BLOCK_DIM),
+ inputs=[P_values, p_reg],
+ block_dim=BLOCK_DIM,
+ )
+
+ def P_inv_mv(x, y, z, alpha, beta):
+ # for cg, y = z, alpha = 1, beta = 0
+ x = _as_float_array(x)
+ z = _as_float_array(z)
+ wp.launch(
+ tile_solve,
+ dim=(P_coarse.nrow, BLOCK_DIM),
+ inputs=[P_values, x, z],
+ block_dim=BLOCK_DIM,
+ )
+
+ return LinearOperator(
+ P_coarse.shape, P_coarse.dtype, P_coarse.device, P_inv_mv
+ )
+
+ P_coarse += p_reg * sp.bsr_diag(
+ block_type(np.eye(block_size)),
+ rows_of_blocks=P_coarse.nrow,
+ cols_of_blocks=P_coarse.ncol,
+ )
+ fem_example_utils.invert_diagonal_bsr_matrix(P_coarse)
+
+ return sp.bsr_copy(P_coarse, block_shape=lhs.block_shape)
+
+ def prepare_newton_step(self, tape=None):
+ self.detect_collisions()
+ self.build_collision_jacobian()
+
+ # compute per-contact forces and hessian
+
+ n_contact = self.n_contact
+ if n_contact > 0 and not self.args.admm_iterations:
+ self._col_energy_gradients = wp.empty(n_contact, dtype=wp.vec3)
+ self._col_energy_hessian = wp.empty(n_contact, dtype=wp.mat33)
+
+ wp.launch(
+ collision_gradient_and_hessian,
+ dim=n_contact,
+ inputs=[
+ self.args.collision_radius,
+ self.args.collision_barrier_ratio,
+ self.args.friction,
+ self.args.dt * self.args.friction_reg,
+ self.args.friction_fluid * self.args.friction_reg,
+ self.cp_du,
+ self.collision_kinematic_gaps,
+ self.collision_normals,
+ self.collision_indices_a,
+ self.collision_indices_b,
+ self._col_energy_gradients,
+ self._col_energy_hessian,
+ ],
+ )
+
+ return super().prepare_newton_step(tape)
+
+ def init_collision_detector(
+ self,
+ kinematic_meshes: List[wp.Mesh],
+ cell_indices,
+ cell_coords,
+ cp_obj_ids: wp.array,
+ cp_node_weights: wp.array = None,
+ cp_node_gradients: wp.array = None,
+ ):
+ self.warp_meshes = kinematic_meshes
+
+ self.cp_cell_indices = cell_indices
+ self.cp_cell_coords = cell_coords
+
+ n_cp = self.cp_cell_indices.shape[0]
+
+ self.collision_quadrature = fem.PicQuadrature(
+ domain=self.vel_quadrature.domain,
+ positions=(self.cp_cell_indices, self.cp_cell_coords),
+ measures=wp.ones(n_cp, dtype=float),
+ )
+
+ self.cp_obj_ids = cp_obj_ids
+ self._cp_node_weights = cp_node_weights
+ self._cp_node_gradients = cp_node_gradients
+ self.n_contact = 0
+
+ self._hashgrid = wp.HashGrid(128, 128, 128)
+ n_cp = self.collision_quadrature.total_point_count()
+
+ self.cp_du = wp.empty(n_cp, dtype=wp.vec3)
+ self.cp_rest_pos = self.cp_world_position()
+
+ max_contacts = 10 * n_cp
+ self.collision_indices_a = wp.empty(max_contacts, dtype=int)
+ self.collision_indices_b = wp.empty(max_contacts, dtype=int)
+ self.collision_normals = wp.empty(max_contacts, dtype=wp.vec3)
+ self.collision_kinematic_gaps = wp.empty(max_contacts, dtype=wp.vec3)
+
+ jac_cols = self.u_field.space_partition.node_count()
+ self._collision_jacobian_a = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+ self._collision_jacobian_b = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+
+ if self._use_tiles:
+ jac_cols = (jac_cols * 3) // self._tile_size
+ self._collision_jacobian = sp.bsr_zeros(
+ 0, jac_cols, block_type=wp.mat((3, self._tile_size), dtype=float)
+ )
+ self._collision_jacobian_t = sp.bsr_zeros(
+ jac_cols, 0, block_type=wp.mat((self._tile_size, 3), dtype=float)
+ )
+ else:
+ self._collision_jacobian = sp.bsr_zeros(0, jac_cols, block_type=wp.mat33)
+ self._collision_jacobian_t = sp.bsr_zeros(jac_cols, 0, block_type=wp.mat33)
+
+ self._HtH_work_arrays = sp.bsr_mm_work_arrays()
+ self._HbHa_work_arrays = sp.bsr_axpy_work_arrays()
+
+ # auto-scale with mass, but keep backward compat with old default
+ self._collision_stiffness = (
+ self.args.collision_stiffness * self.args.density / 1000.0
+ )
+
+ def qp_world_position(self):
+ qp_pic = self.vel_quadrature
+ qp_cur_pos = wp.empty(qp_pic.total_point_count(), dtype=wp.vec3)
+ fem.interpolate(
+ world_position,
+ fields={"u": self.u_field},
+ dest=qp_cur_pos,
+ quadrature=qp_pic,
+ )
+
+ return qp_cur_pos
+
+ def cp_world_position(self):
+ cp_pic = self.collision_quadrature
+ cp_cur_pos = wp.empty_like(self.cp_du)
+ with ScopedCachedBasisWeights(
+ self, self._cp_node_weights, self._cp_node_gradients
+ ):
+ fem.interpolate(
+ world_position,
+ fields={"u": self.u_field},
+ dest=cp_cur_pos,
+ quadrature=cp_pic,
+ )
+
+ return cp_cur_pos
+
+ def _sample_cp_displacement(self, du_field, dest):
+ cp_pic = self.collision_quadrature
+ with ScopedCachedBasisWeights(
+ self, self._cp_node_weights, self._cp_node_gradients
+ ):
+ fem.interpolate(
+ du_field,
+ dest=dest,
+ quadrature=cp_pic,
+ )
+
+ def detect_collisions(self):
+ cp_pic = self.collision_quadrature
+ n_cp = cp_pic.total_point_count()
+
+ cp_cur_pos = self.cp_world_position()
+ self._sample_cp_displacement(self.du_field, self.cp_du)
+
+ count = wp.zeros(1, dtype=int)
+ max_contacts = self.collision_normals.shape[0]
+
+ indices_a = self.collision_indices_a
+ indices_b = self.collision_indices_b
+ normals = self.collision_normals
+ kinematic_gaps = self.collision_kinematic_gaps
+
+ collision_radius = (
+ self.args.collision_radius * self.args.collision_detection_ratio
+ )
+
+ if self.args.ground:
+ ground_height = self.args.ground_height
+ wp.launch(
+ detect_ground_collisions,
+ dim=n_cp,
+ inputs=[
+ max_contacts,
+ self.args.up_axis,
+ cp_cur_pos,
+ self.cp_du,
+ collision_radius,
+ ground_height,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+
+ self_collision_immune_radius = (
+ self.args.self_immunity_radius_ratio * collision_radius
+ )
+
+ self._hashgrid.build(cp_cur_pos, radius=2.0 * collision_radius)
+ wp.launch(
+ detect_particle_collisions,
+ dim=n_cp,
+ inputs=[
+ max_contacts,
+ self._hashgrid.id,
+ 2.0 * collision_radius,
+ self_collision_immune_radius,
+ cp_cur_pos,
+ self.cp_rest_pos,
+ self.cp_du,
+ self.cp_obj_ids,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+
+ if self.warp_meshes:
+ mesh_ids = wp.array([mesh.id for mesh in self.warp_meshes], dtype=wp.uint64)
+ wp.launch(
+ detect_mesh_collisions,
+ dim=(len(mesh_ids), n_cp),
+ inputs=[
+ max_contacts,
+ mesh_ids,
+ cp_cur_pos,
+ self.cp_du,
+ collision_radius,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ ],
+ )
+
+ self.n_contact = int(count.numpy()[0])
+
+ if self.n_contact > max_contacts:
+ print("Warning: contact buffer size exceeded, some have bee ignored")
+ self.n_contact = max_contacts
+
+ def build_collision_jacobian(self):
+ n_contact = self.n_contact
+
+ # Build collision jacobian
+ # (derivative of collision gap `pos_a - pos_b` w.r.t. degrees of freedom)
+
+ if n_contact == 0:
+ return
+
+ a_cells = wp.empty(n_contact, dtype=int)
+ a_coords = wp.empty(n_contact, dtype=wp.vec3)
+ b_cells = wp.empty(n_contact, dtype=int)
+ b_coords = wp.empty(n_contact, dtype=wp.vec3)
+ wp.launch(
+ gather_cell_coordinates,
+ dim=n_contact,
+ inputs=[
+ self.cp_cell_indices,
+ self.cp_cell_coords,
+ self.collision_indices_a,
+ a_cells,
+ a_coords,
+ ],
+ )
+ wp.launch(
+ gather_cell_coordinates,
+ dim=n_contact,
+ inputs=[
+ self.cp_cell_indices,
+ self.cp_cell_coords,
+ self.collision_indices_b,
+ b_cells,
+ b_coords,
+ ],
+ )
+
+ measures = wp.ones(n_contact, dtype=float)
+
+ a_contact_pic = fem.PicQuadrature(
+ self.collision_quadrature.domain,
+ positions=(a_cells, a_coords),
+ measures=measures,
+ )
+ b_contact_pic = fem.PicQuadrature(
+ self.collision_quadrature.domain,
+ positions=(b_cells, b_coords),
+ measures=measures,
+ )
+ u_trial = fem.make_trial(
+ self.u_field.space, space_partition=self.u_field.space_partition
+ )
+
+ with ScopedCachedBasisWeights(
+ self, self._cp_node_weights, self._cp_node_gradients
+ ):
+ if isinstance(self._vel_basis, DuplicatedBasisSpace):
+ self._vel_basis.set_subset_indices(self.collision_indices_a)
+
+ sp.bsr_set_zero(
+ self._collision_jacobian_a,
+ n_contact,
+ self.u_field.space_partition.node_count(),
+ )
+ fem.interpolate(
+ u_trial,
+ quadrature=a_contact_pic,
+ dest=self._collision_jacobian_a,
+ bsr_options={"prune_numerical_zeros": False},
+ )
+
+ sp.bsr_set_zero(
+ self._collision_jacobian_b,
+ n_contact,
+ self.u_field.space_partition.node_count(),
+ )
+ if isinstance(self._vel_basis, DuplicatedBasisSpace):
+ self._vel_basis.set_subset_indices(self.collision_indices_b)
+ fem.interpolate(
+ u_trial,
+ quadrature=b_contact_pic,
+ dest=self._collision_jacobian_b,
+ bsr_options={"prune_numerical_zeros": False},
+ )
+
+ if isinstance(self._vel_basis, DuplicatedBasisSpace):
+ self._vel_basis.set_subset_indices(None)
+
+ gc.collect(1)
+ self._collision_jacobian_a.nnz_sync()
+ self._collision_jacobian_b.nnz_sync()
+
+ if self._use_tiles:
+ H = bsr_coarsen_aligned(
+ self._collision_jacobian_a,
+ block_shape=(3, self._tile_size),
+ coarse=self._collision_jacobian,
+ )
+ del self._collision_jacobian_a.values
+ Hb = bsr_coarsen_aligned(
+ self._collision_jacobian_b,
+ block_shape=(3, self._tile_size),
+ )
+ del self._collision_jacobian_b.values
+ sp.bsr_axpy(
+ x=Hb,
+ y=H,
+ alpha=-1,
+ beta=1,
+ work_arrays=self._HbHa_work_arrays,
+ )
+ del Hb
+
+ self._collision_jacobian = H
+
+ else:
+ sp.bsr_assign(self._collision_jacobian, src=self._collision_jacobian_a)
+ del self._collision_jacobian_a.values
+ sp.bsr_axpy(
+ x=self._collision_jacobian_b,
+ y=self._collision_jacobian,
+ alpha=-1,
+ beta=1,
+ work_arrays=self._HbHa_work_arrays,
+ )
+ del self._collision_jacobian_b.values
+
+ # we no longer need a,b, values, just the topology
+ # garbag-collect value arrays
+ self._collision_jacobian_a.values = wp.empty(0, dtype=wp.mat33)
+ self._collision_jacobian_b.values = wp.empty(0, dtype=wp.mat33)
+
+ gc.collect(0)
+ self._collision_jacobian.nnz_sync()
+ sp.bsr_set_transpose(
+ dest=self._collision_jacobian_t, src=self._collision_jacobian
+ )
+
+
+class ScopedCachedBasisWeights:
+ def __init__(self, sim: SparseBlendedSim, weights, grad_weights):
+ self._sim = sim
+ self._weights = weights
+ self._grad_weights = grad_weights
+
+ def __enter__(self):
+ self._weights, self._grad_weights = self._sim.set_cached_basis_qp_weights(
+ self._weights, self._grad_weights
+ )
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self._weights, self._grad_weights = self._sim.set_cached_basis_qp_weights(
+ self._weights, self._grad_weights
+ )
+
+
+@wp.func
+def _lame_field(
+ qp_index: int,
+ lame_ref: wp.vec2,
+ qp_stiff_scale: wp.array(dtype=float),
+):
+ return lame_ref * qp_stiff_scale[qp_index]
+
+
+@fem.integrand
+def prescribed_position_lhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+ stiffness: fem.Field,
+):
+ u_displ = u(s)
+ v_displ = v(s)
+ return stiffness(s) * wp.dot(u_displ, v_displ)
+
+
+@fem.integrand
+def prescribed_position_rhs_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ v: fem.Field,
+ stiffness: fem.Field,
+ target: fem.Field,
+):
+ pos = world_position(s, domain, u_cur)
+ v_displ = v(s)
+ target_pos = target(s)
+ return stiffness(s) * wp.dot(target_pos - pos, v_displ)
+
+
+@fem.integrand
+def prescribed_position_energy_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ stiffness: fem.Field,
+ target: fem.Field,
+):
+ pos = world_position(s, domain, u_cur)
+ target_pos = target(s)
+ return 0.5 * stiffness(s) * wp.length_sq(pos - target_pos)
+
+
+@wp.kernel
+def gather_cell_coordinates(
+ qp_cells: wp.array(dtype=int),
+ qp_coords: wp.array(dtype=wp.vec3),
+ indices: wp.array(dtype=int),
+ cells: wp.array(dtype=int),
+ coords: wp.array(dtype=wp.vec3),
+):
+ i = wp.tid()
+ qp = indices[i]
+
+ if qp == fem.NULL_QP_INDEX:
+ cells[i] = fem.NULL_ELEMENT_INDEX
+ else:
+ cells[i] = qp_cells[qp]
+ coords[i] = qp_coords[qp]
+
+
+@fem.integrand
+def world_position(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+):
+ return domain(s) + u(s)
+
+
+class SparseBlendedScene:
+ def __init__(self, args):
+ self.args = args
+ self.objects = []
+ self.kinematic_meshes = []
+
+ self._obj_vertex_offsets = []
+
+ self._vertices = None
+ self._vertex_Fs = None
+ self._vertex_quadrature = None
+ self._vtx_weights = None
+ self._vtx_weight_gradients = None
+
+ # fem.set_default_temporary_store(fem.TemporaryStore())
+
+ class Object(NamedTuple):
+ origin: wp.vec3
+ rotation: wp.quat
+ scale: float
+ qps: Optional[wp.array] = None
+ cps: Optional[wp.array] = None
+ vertices: Optional[wp.array] = None
+ vertex_Fs: Optional[wp.array] = None
+ weight_eval_fn: Optional[Callable] = None
+ stiffness_eval_fn: Optional[Callable] = None
+ input_bbox: np.array = None
+ vol_fraction: float = 1.0
+
+ def add_object(
+ self,
+ origin,
+ rotation=None,
+ scale=None,
+ qps=None,
+ cps=None,
+ vertices=None,
+ input_bbox=None,
+ weight_eval_fn=None,
+ stiffness_eval_fn=None,
+ vol_fraction=1.0,
+ ):
+ if vertices is None:
+ vertices = np.empty(shape=(0, 3))
+ if rotation is None:
+ rotation = wp.quat_identity(dtype=float)
+
+ if scale is None:
+ scale = wp.vec3(1.0)
+ else:
+ scale = wp.vec3(scale)
+
+ if qps is None:
+ qps = np.random.rand(self.args.n_qp, 3)
+
+ if cps is None:
+ cps = qps
+
+ if input_bbox is None:
+ all_pts = np.vstack((qps, cps, vertices))
+ input_bbox = np.array([np.min(all_pts, axis=0), np.max(all_pts, axis=0)])
+
+ scale = wp.cw_mul(scale, wp.vec3(input_bbox[1] - input_bbox[0]))
+
+ if stiffness_eval_fn is None:
+ stiffness_eval_fn = lambda pts: np.ones(pts.shape[0])
+
+ self.objects.append(
+ SparseBlendedScene.Object(
+ origin=origin,
+ rotation=rotation,
+ scale=scale,
+ qps=qps,
+ cps=cps,
+ weight_eval_fn=weight_eval_fn,
+ stiffness_eval_fn=stiffness_eval_fn,
+ vertices=vertices,
+ vertex_Fs=np.empty((vertices.shape[0], 3, 3)),
+ input_bbox=input_bbox,
+ vol_fraction=vol_fraction,
+ )
+ )
+
+ def add_kinematic_mesh(self, mesh: wp.Mesh):
+ self.kinematic_meshes.append(mesh)
+
+ @wp.kernel
+ def _world_position(
+ vtx_obj_ids: wp.array(dtype=int),
+ vtx_pos: wp.array(dtype=wp.vec3),
+ obj_pos: wp.array(dtype=wp.vec3),
+ obj_rot: wp.array(dtype=wp.quatf),
+ obj_scale: wp.array(dtype=wp.vec3),
+ ):
+ i = wp.tid()
+ oid = vtx_obj_ids[i]
+ loc_pos = vtx_pos[i]
+ vtx_pos[i] = (
+ wp.quat_rotate(obj_rot[oid], wp.cw_mul(loc_pos, obj_scale[oid]))
+ + obj_pos[oid]
+ )
+
+ @fem.integrand
+ def _assign_to_cells(
+ domain: fem.Domain,
+ s: fem.Sample,
+ points: wp.array(dtype=wp.vec3),
+ point_obj_ids: wp.array(dtype=int),
+ cell_obj_ids: wp.array(dtype=int),
+ cell_coords: wp.array(dtype=wp.vec3),
+ cell_indices: wp.array(dtype=int),
+ ):
+ pos = points[s.qp_index]
+ obj_id = point_obj_ids[s.qp_index]
+ max_dist = 1.0
+ s_proj = fem.lookup(domain, pos, max_dist, cell_obj_ids, obj_id)
+
+ cell_indices[s.qp_index] = s_proj.element_index
+ cell_coords[s.qp_index] = fem.element_coordinates(
+ domain, s_proj.element_index, pos
+ )
+
+ def _merged_world_space_points(self, per_object_points: List[np.array]):
+ n_obj = len(self.objects)
+
+ points = np.vstack(per_object_points)
+ points = wp.array(points, dtype=wp.vec3)
+
+ obj_n_points = np.array([pts.shape[0] for pts in per_object_points])
+ obj_ids = wp.array(np.arange(n_obj).repeat(obj_n_points), dtype=int)
+ obj_offsets = np.concatenate(([0], np.cumsum(obj_n_points)), dtype=int)
+
+ self._transform_points(points, obj_ids)
+
+ return points, obj_ids, obj_offsets
+
+ def _transform_points(self, points, obj_ids):
+ obj_pos = wp.array([obj.origin for obj in self.objects], dtype=wp.vec3)
+ obj_scales = wp.array([obj.scale for obj in self.objects], dtype=wp.vec3)
+ obj_rot = wp.array([obj.rotation for obj in self.objects], dtype=wp.quatf)
+
+ wp.launch(
+ SparseBlendedScene._world_position,
+ dim=points.shape,
+ inputs=[obj_ids, points, obj_pos, obj_rot, obj_scales],
+ )
+
+ def _gather_world_space_points(self, points_lambda: Callable):
+ points = []
+ for obj in self.objects:
+ obj_extent = obj.input_bbox[1] - obj.input_bbox[0]
+ points.append((points_lambda(obj) - obj.input_bbox[0]) / obj_extent)
+
+ return self._merged_world_space_points(points)
+
+ def _build_grid(self):
+ res = wp.vec3i(self.args.res)
+
+ grid_vtx = []
+ grid_cells = []
+
+ vtx_obj_ids = []
+ cell_obj_ids = []
+
+ # Build object grids
+ if self.args.dual_grid:
+ bounds_lo = wp.vec3(0.5 / (self.args.res + 1))
+ bounds_hi = wp.vec3(1.0 - 0.5 / (self.args.res + 1))
+ else:
+ bounds_lo = wp.vec3(0.0)
+ bounds_hi = wp.vec3(1.0)
+
+ obj_vtx_offset = 0
+ for oid, obj in enumerate(self.objects):
+ obj_grid_vtx, obj_grid_cells = fem_example_utils.gen_hexmesh(
+ res, bounds_lo, bounds_hi
+ )
+ obj_grid_vtx = obj_grid_vtx.numpy()
+ obj_grid_cells = obj_grid_cells.numpy()
+
+ obj_grid_cells += obj_vtx_offset
+
+ grid_vtx.append(obj_grid_vtx)
+ grid_cells.append(obj_grid_cells)
+
+ cell_obj_ids.append(np.full(obj_grid_cells.shape[0], fill_value=oid))
+ obj_vtx_offset += obj_grid_vtx.shape[0]
+
+ grid_cells = wp.array(np.vstack(grid_cells), dtype=int)
+ cell_obj_ids = wp.array(np.concatenate(cell_obj_ids), dtype=int)
+
+ grid_vtx, vtx_obj_ids, vtx_obj_offsets = self._merged_world_space_points(
+ grid_vtx
+ )
+
+ geo = fem.Hexmesh(
+ grid_cells, grid_vtx, assume_parallelepiped_cells=True, build_bvh=True
+ )
+
+ return geo, cell_obj_ids, vtx_obj_ids, vtx_obj_offsets
+
+ def _embed_points(self, geo, cell_obj_ids, points, point_obj_ids):
+ domain = fem.Cells(geo)
+
+ qp_cell_indices = wp.empty_like(point_obj_ids)
+ qp_cell_coords = wp.empty_like(points)
+ fem.interpolate(
+ SparseBlendedScene._assign_to_cells,
+ domain=domain,
+ dim=len(points),
+ values={
+ "points": points,
+ "point_obj_ids": point_obj_ids,
+ "cell_obj_ids": cell_obj_ids,
+ "cell_coords": qp_cell_coords,
+ "cell_indices": qp_cell_indices,
+ },
+ )
+
+ return qp_cell_indices, qp_cell_coords
+
+ def make_sim(self) -> SparseBlendedSim:
+ n_obj = len(self.objects)
+ if n_obj == 0:
+ return None
+
+ geo, cell_obj_ids, grid_node_obj_ids, obj_grid_node_offsets = self._build_grid()
+
+ # quadrature points
+ qps, qp_obj_ids, self._obj_qp_offsets = self._gather_world_space_points(
+ lambda obj: obj.qps
+ )
+ qp_cell_indices, qp_cell_coords = self._embed_points(
+ geo, cell_obj_ids, qps, qp_obj_ids
+ )
+ (
+ weights,
+ weight_gradients,
+ ) = self._eval_point_weights(
+ geo, qps, qp_cell_indices, qp_cell_coords, qp_obj_ids, self._obj_qp_offsets
+ )
+
+ n_duplicates = weights.shape[-1] if weights is not None else 1
+ print(f"Using {n_duplicates} handles-per-cell")
+
+ sim = SparseBlendedSim(
+ geo=geo,
+ args=self.args,
+ active_cells=None,
+ n_duplicates=n_duplicates,
+ )
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ # Use our own quadrature points instead of regular ones
+ obj_qp_count = self._obj_qp_offsets[1:] - self._obj_qp_offsets[:-1]
+ qp_measures = (
+ np.array([obj.vol_fraction for obj in self.objects]) / obj_qp_count
+ )
+ self.qp_measures = wp.array(
+ np.repeat(qp_measures, obj_qp_count),
+ dtype=float,
+ )
+
+ qp_stiffness_scales = wp.ones(qps.shape[0], dtype=float)
+ self._eval_point_stiffness(geo, qps, self._obj_qp_offsets, qp_stiffness_scales)
+
+ sim.set_quadrature(
+ qp_cell_indices,
+ qp_cell_coords,
+ self.qp_measures,
+ qp_obj_ids,
+ qp_stiffness_scales,
+ weights,
+ weight_gradients,
+ )
+
+ # Disable fixed points
+ sim.set_boundary_condition(boundary_projector_form=None)
+
+ # collision particles
+ cps, cp_obj_ids, self._obj_cp_offsets = self._gather_world_space_points(
+ lambda obj: obj.cps
+ )
+ cp_cell_indices, cp_cell_coords = self._embed_points(
+ geo, cell_obj_ids, cps, cp_obj_ids
+ )
+ (
+ cp_weights,
+ cp_weight_gradients,
+ ) = self._eval_point_weights(
+ geo, cps, cp_cell_indices, cp_cell_coords, cp_obj_ids, self._obj_cp_offsets
+ )
+
+ sim.init_collision_detector(
+ self.kinematic_meshes,
+ cp_cell_indices,
+ cp_cell_coords,
+ cp_obj_ids,
+ cp_weights,
+ cp_weight_gradients,
+ )
+
+ # visualization points
+ # Do the same thing as for quadrature points:
+ # assign to cells, evaluate wieghts and gradients, create quadrature
+
+ self._vertices, self._vtx_obj_ids, self._obj_vtx_offsets = (
+ self._gather_world_space_points(lambda obj: obj.vertices)
+ )
+ vtx_cell_indices, vtx_cell_coords = self._embed_points(
+ geo, cell_obj_ids, self._vertices, self._vtx_obj_ids
+ )
+ (
+ self._vtx_weights,
+ self._vtx_weight_gradients,
+ ) = self._eval_point_weights(
+ geo,
+ self._vertices,
+ vtx_cell_indices,
+ vtx_cell_coords,
+ self._vtx_obj_ids,
+ self._obj_vtx_offsets,
+ )
+ measures = wp.ones(vtx_cell_indices.shape[0], dtype=float)
+ self._vertex_quadrature = fem.PicQuadrature(
+ fem.Cells(geo),
+ positions=(vtx_cell_indices, vtx_cell_coords),
+ measures=measures,
+ )
+ self._vertex_Fs = wp.zeros((self._vertices.shape[0]), dtype=wp.mat33)
+
+ # grid vertices, again for visualization
+
+ grid_cell_indices, grid_cell_coords = self._embed_points(
+ geo, cell_obj_ids, geo.positions, grid_node_obj_ids
+ )
+
+ (
+ self._grid_weights,
+ self._grid_weight_gradients,
+ ) = self._eval_point_weights(
+ geo,
+ geo.positions,
+ grid_cell_indices,
+ grid_cell_coords,
+ grid_node_obj_ids,
+ obj_grid_node_offsets,
+ )
+
+ measures = wp.ones(grid_cell_indices.shape[0], dtype=float)
+ self._grid_quadrature = fem.PicQuadrature(
+ fem.Cells(geo),
+ positions=(grid_cell_indices, grid_cell_coords),
+ measures=measures,
+ )
+
+ return sim
+
+ def cell_node_indices(self, sim: SparseBlendedSim):
+ # Save cell node indices; useful for feature network evaluation
+ cell_node_indices = sim.u_field.space.topology.element_node_indices()
+ node_size = sim._handle_size * sim._n_duplicates
+ node_count = cell_node_indices.shape[1] // node_size
+
+ cell_node_indices_3d = wp.empty(
+ (cell_node_indices.shape[0], node_count), dtype=wp.vec3i
+ )
+
+ wp.launch(
+ _extract_3d_node_indices,
+ dim=cell_node_indices_3d.shape,
+ inputs=[
+ node_size,
+ sim.geo.res,
+ cell_node_indices,
+ cell_node_indices_3d,
+ ],
+ )
+
+ return cell_node_indices_3d
+
+ def _eval_point_weights(
+ self,
+ geo: fem.Grid3D,
+ points: wp.array,
+ cell_indices: wp.array,
+ cell_coords: wp.array,
+ pt_obj_ids: wp.array,
+ obj_pt_offsets: np.array,
+ ):
+ n_obj = len(self.objects)
+
+ weights = None
+ weight_gradients = None
+
+ if self.objects[0].weight_eval_fn is None:
+ return weights, weight_gradients
+
+ cells_per_obj = ((geo.res[0] + n_obj - 1) // n_obj) * geo.res[1] * geo.res[2]
+ obj_cell_indices = wp.empty_like(cell_indices)
+ wp.launch(
+ _compute_obj_cell_indices,
+ dim=cell_indices.shape,
+ inputs=[cell_indices, cells_per_obj, pt_obj_ids, obj_cell_indices],
+ )
+
+ cell_size = np.array(geo.cell_size)
+
+ for k, obj in enumerate(self.objects):
+ pts_slice = slice(int(obj_pt_offsets[k]), int(obj_pt_offsets[k + 1]))
+ if pts_slice.start == pts_slice.stop:
+ continue
+
+ grad_scale = cell_size * (obj.input_bbox[1] - obj.input_bbox[0])
+ obj_weights, obj_weight_gradients = obj.weight_eval_fn(
+ points[pts_slice],
+ obj_cell_indices[pts_slice],
+ cell_coords[pts_slice],
+ grad_scale,
+ )
+
+ if weights is None:
+ weights = wp.empty(
+ (points.shape[0], *obj_weights.shape[1:]), dtype=float
+ )
+ weight_gradients = wp.empty(weights.shape, dtype=wp.vec3)
+
+ weights[pts_slice].assign(obj_weights)
+ weight_gradients[pts_slice].assign(obj_weight_gradients)
+
+ return (
+ weights,
+ weight_gradients,
+ )
+
+ def _eval_point_stiffness(
+ self,
+ geo: fem.Grid3D,
+ points: wp.array,
+ obj_pt_offsets: np.array,
+ pt_stiffness: wp.array,
+ ):
+ for k, obj in enumerate(self.objects):
+ if not obj.stiffness_eval_fn:
+ continue
+
+ pts_slice = slice(int(obj_pt_offsets[k]), int(obj_pt_offsets[k + 1]))
+ if pts_slice.start == pts_slice.stop:
+ continue
+ pt_stiffness[pts_slice].assign(obj.stiffness_eval_fn(points[pts_slice]))
+
+ def update_object_vertices(self, sim: SparseBlendedSim):
+ # Update shared vertex array
+
+ with ScopedCachedBasisWeights(
+ sim, self._vtx_weights, self._vtx_weight_gradients
+ ):
+ fem.interpolate(
+ world_position,
+ fields={"u": sim.u_field},
+ dest=self._vertices,
+ quadrature=self._vertex_quadrature,
+ )
+
+ fem.interpolate(
+ defgrad,
+ fields={"u": sim.u_field},
+ dest=self._vertex_Fs,
+ quadrature=self._vertex_quadrature,
+ )
+
+ # Copy to individual objects
+ vertices_np = self._vertices.numpy()
+ Fs_np = self._vertex_Fs.numpy()
+ for k, obj in enumerate(self.objects):
+ vtx_beg = self._obj_vtx_offsets[k]
+ vtx_end = self._obj_vtx_offsets[k + 1]
+ np.copyto(dst=obj.vertices, src=vertices_np[vtx_beg:vtx_end])
+ np.copyto(dst=obj.vertex_Fs, src=Fs_np[vtx_beg:vtx_end])
+
+ def update_grid_nodes(self, sim: SparseBlendedSim):
+ # Update shared vertex array
+
+ with ScopedCachedBasisWeights(
+ sim, self._grid_weights, self._grid_weight_gradients
+ ):
+ fem.interpolate(
+ world_position,
+ fields={"u": sim.u_field},
+ dest=self.grid_nodes,
+ quadrature=self._grid_quadrature,
+ )
+
+
+@wp.kernel
+def _compute_obj_cell_indices(
+ cell_indices: wp.array(dtype=int),
+ cells_per_obj: int,
+ pt_obj_ids: wp.array(dtype=int),
+ obj_cell_indices: wp.array(dtype=int),
+):
+ i = wp.tid()
+ obj_cell_indices[i] = cell_indices[i] - cells_per_obj * pt_obj_ids[i]
+
+
+@wp.kernel
+def _extract_3d_node_indices(
+ node_size: int,
+ grid_res: wp.vec3i,
+ cell_node_indices: wp.array2d(dtype=int),
+ cell_node_indices_3d: wp.array2d(dtype=wp.vec3i),
+):
+ cell, k = wp.tid()
+
+ node_idx = cell_node_indices[cell, k * node_size] // node_size
+
+ strides = wp.vec2i((grid_res[1] + 1) * (grid_res[2] + 1), (grid_res[2] + 1))
+ nx = node_idx // strides[0]
+ ny = (node_idx - strides[0] * nx) // strides[1]
+ nz = node_idx - strides[0] * nx - strides[1] * ny
+
+ cell_node_indices_3d[cell, k] = wp.vec3i(nx, ny, nz)
+
+
+def _analytical_weights(args):
+ n_nodes = 8
+
+ def cosine_weights(pts, cell_indices, cell_coords, world_space_grad_scale):
+ pts = pts.numpy()
+
+ ones = np.ones(pts.shape[:-1])
+
+ freq = 2.0 * np.pi * (args.res + 1)
+
+ weights = np.stack(
+ (
+ ones,
+ np.cos(pts[..., 0] * freq),
+ np.cos(pts[..., 1] * freq),
+ np.cos(pts[..., 2] * freq),
+ ),
+ axis=1,
+ )
+
+ grads = np.zeros((*weights.shape, 3))
+ grads[..., 1, 0] = (
+ -freq * np.sin(pts[..., 0] * freq) * world_space_grad_scale[0]
+ )
+ grads[..., 2, 1] = (
+ -freq * np.sin(pts[..., 1] * freq) * world_space_grad_scale[1]
+ )
+ grads[..., 3, 2] = (
+ -freq * np.sin(pts[..., 2] * freq) * world_space_grad_scale[2]
+ )
+
+ weights = np.broadcast_to(
+ weights[np.newaxis, ...], shape=(n_nodes, *weights.shape)
+ )
+ grads = np.broadcast_to(grads[np.newaxis, ...], shape=(n_nodes, *grads.shape))
+
+ weights = np.transpose(weights, axes=(1, 0, 2))
+ grads = np.transpose(grads, axes=(1, 0, 2, 3))
+
+ weights = np.ascontiguousarray(weights)
+ grads = np.ascontiguousarray(grads)
+
+ return weights, grads
+
+ return cosine_weights if args.cosine else None
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ SparseBlendedSim.add_parser_arguments(parser)
+ parser.add_argument("--res", type=int, default=1, help="Grid resolution")
+ parser.add_argument(
+ "--n_qp", type=int, default=256, help="Number of quadrature points per object"
+ )
+
+ parser.add_argument("--headless", action=argparse.BooleanOptionalAction)
+ parser.add_argument("--screenshot", action=argparse.BooleanOptionalAction)
+ parser.add_argument(
+ "--cosine",
+ action=argparse.BooleanOptionalAction,
+ help="Use cosine basis functions (emulates beural basis)",
+ )
+ args = parser.parse_args()
+
+ scene = SparseBlendedScene(args)
+
+ weight_eval_fn = _analytical_weights(args)
+
+ # scene.add_object(wp.vec3(0.0, -0.25, 0.0))
+
+ scene.add_object(
+ wp.vec3(0.0, 1.0, 0.0),
+ scale=wp.vec3(1.5, 1.0, 0.75),
+ # rotation=wp.quat_from_axis_angle(wp.vec3(0.0, 0.0, 1.0), 0.8),
+ weight_eval_fn=weight_eval_fn,
+ stiffness_eval_fn=lambda pts: np.where(pts.numpy()[:, 1] < 0.5, 0.1, 1.0),
+ )
+ # scene.add_object(wp.vec3(1.4, 1.5, 0.0), weight_eval_fn=weight_eval_fn)
+ scene.add_object(wp.vec3(0.5, 3.5, 0.0), scale=0.75, weight_eval_fn=weight_eval_fn)
+ scene.add_object(wp.vec3(0.5, 5.25, 0.0), weight_eval_fn=weight_eval_fn)
+ scene.add_object(wp.vec3(0.5, 6.5, 0.0), weight_eval_fn=weight_eval_fn)
+
+ scene.add_object(
+ wp.vec3(0.5, 8.75, 0.0),
+ qps=np.random.rand(args.n_qp, 3) * 2.0 + 2.0,
+ weight_eval_fn=weight_eval_fn,
+ )
+
+ sim = scene.make_sim()
+
+ # fix first object
+ qp_target_pos = sim.qp_world_position()
+ qp_start_pos = qp_target_pos.numpy()
+
+ @wp.func
+ def prescribed_pos(qp_index: int, qp_target_pos: wp.array(dtype=wp.vec3)):
+ return qp_target_pos[qp_index]
+
+ @wp.func
+ def prescribed_pos_weight(qp_index: int, k: float, n_qp: int):
+ return wp.where(qp_index < n_qp, k, 0.0)
+
+ sim.set_prescribed_positions(
+ pos_field=QPBasedImplicitField(
+ sim.domain, prescribed_pos, values={"qp_target_pos": qp_target_pos}
+ ),
+ weight_field=QPBasedImplicitField(
+ sim.domain,
+ prescribed_pos_weight,
+ values={"n_qp": args.n_qp, "k": args.collision_stiffness},
+ ),
+ )
+
+ # animate
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ if args.headless:
+ for sim.cur_frame in range(sim.args.n_frames):
+ with wp.ScopedTimer(f"--- Frame --- {sim.cur_frame}", synchronize=True):
+ sim.run_frame()
+
+ else:
+ sim.cur_frame = 0
+
+ ps.init()
+ ps.set_ground_plane_height(0.0)
+
+ qpoints = ps.register_point_cloud(
+ "qp", sim.qp_world_position().numpy(), enabled=False
+ )
+
+ cpoints = ps.register_point_cloud("cp", sim.cp_world_position().numpy())
+ cpoints.set_radius(args.collision_radius, relative=False)
+
+ n_obj = len(scene.objects)
+ colors = np.broadcast_to(
+ np.random.rand(n_obj, 1, 3),
+ shape=(n_obj, args.n_qp, 3),
+ ).reshape(-1, 3)
+
+ qpoints.add_color_quantity("index", colors, enabled=True)
+ cpoints.add_color_quantity("index", colors, enabled=True)
+
+ def callback():
+ sim.cur_frame = sim.cur_frame + 1
+ if sim.args.n_frames >= 0 and sim.cur_frame > sim.args.n_frames:
+ return
+
+ # animate target points
+ qp_target_pos.assign(
+ qp_start_pos + np.array([0, 0, np.sin(sim.cur_frame / 10)])[np.newaxis]
+ )
+
+ with wp.ScopedTimer(f"--- Frame --- {sim.cur_frame}", synchronize=True):
+ sim.run_frame()
+
+ # sample displacement at quadrature points
+ qpoints.update_point_positions(sim.qp_world_position().numpy())
+ cpoints.update_point_positions(sim.cp_world_position().numpy())
+
+ if args.screenshot:
+ ps.screenshot()
+
+ ps.set_user_callback(callback)
+ ps.look_at(target=(1.5, 1.5, 1.0), camera_location=(0.0, 3.0, 7))
+ ps.show()
diff --git a/deps/vomp/vomp/fem/fem_examples/topo_opt/screened_poisson.py b/deps/vomp/vomp/fem/fem_examples/topo_opt/screened_poisson.py
new file mode 100644
index 0000000000000000000000000000000000000000..a7cd26aa4ea6c1507a217d4a05c7989688ad801d
--- /dev/null
+++ b/deps/vomp/vomp/fem/fem_examples/topo_opt/screened_poisson.py
@@ -0,0 +1,574 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Copyright (c) 2022 NVIDIA CORPORATION. All rights reserved.
+# NVIDIA CORPORATION and its licensors retain all intellectual property
+# and proprietary rights in and to this software, related documentation
+# and any modifications thereto. Any use, reproduction, disclosure or
+# distribution of this software and related documentation without an express
+# license agreement from NVIDIA CORPORATION is strictly prohibited.
+
+import numpy as np
+import polyscope as ps
+import torch
+import trimesh
+from icosphere import icosphere
+from largesteps.geometry import compute_matrix
+from largesteps.parameterize import from_differential, to_differential
+from pyremesh import remesh_botsch
+
+import warp as wp
+import warp.examples.fem.utils as fem_example_utils
+import warp.fem as fem
+
+
+@fem.integrand
+def screened_diffusion_form(
+ s: fem.Sample, u: fem.Field, v: fem.Field, sigma: float, nu: float
+):
+ return sigma * u(s) * v(s) + nu * wp.dot(
+ fem.grad(u, s),
+ fem.grad(v, s),
+ )
+
+
+@fem.integrand
+def boundary_projector_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+):
+ return u(s) * v(s)
+
+
+@fem.integrand
+def interior_penalty_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u: fem.Field,
+ v: fem.Field,
+ strength: float,
+):
+ return strength * u(s) * v(s)
+
+
+@fem.integrand
+def sample_solution(s: fem.Sample, u: fem.Field):
+ return u(s)
+
+
+@wp.kernel
+def loss_fn(
+ target: wp.array(dtype=float),
+ samples: wp.array(dtype=float),
+ loss: wp.array(dtype=wp.float64),
+):
+ i = wp.tid()
+ diff = target[i] - samples[i]
+ wp.atomic_add(loss, 0, wp.float64(diff * diff))
+
+
+@wp.kernel
+def gen_face_samples(
+ mesh_faces: wp.array2d(dtype=int),
+ mesh_vertices: wp.array(dtype=wp.vec3),
+ qp_coords: wp.array(dtype=wp.vec3),
+ qp_weights: wp.array(dtype=float),
+ points: wp.array2d(dtype=wp.vec3),
+ point_measures: wp.array2d(dtype=float),
+):
+ i, j = wp.tid()
+
+ v0 = mesh_vertices[mesh_faces[i, 0]]
+ v1 = mesh_vertices[mesh_faces[i, 1]]
+ v2 = mesh_vertices[mesh_faces[i, 2]]
+ points[i, j] = qp_coords[j][0] * v0 + qp_coords[j][1] * v1 + qp_coords[j][2] * v2
+
+ area = 0.5 * wp.length(wp.cross(v1 - v0, v2 - v0))
+ point_measures[i, j] = qp_weights[j] * area
+
+
+def remesh(vertices, faces, h):
+ v = vertices.numpy()
+ f = faces.numpy()
+ v_new, f_new = remesh_botsch(v.astype(np.double), f.astype(np.int32), 5, h, True)
+
+ return wp.array(v_new, dtype=wp.vec3, requires_grad=True), wp.array(
+ f_new, dtype=int
+ )
+
+
+def largesteps_matrix(vertices, faces, smoothing):
+ with torch.no_grad():
+ M = compute_matrix(
+ wp.to_torch(vertices, requires_grad=False),
+ wp.to_torch(faces, requires_grad=False),
+ lambda_=smoothing,
+ # alpha=0.75,
+ )
+ return M
+
+
+def parametrize(vertices, params, M):
+ with torch.no_grad():
+ u = wp.from_torch(
+ to_differential(L=M, v=wp.to_torch(vertices, requires_grad=False)),
+ requires_grad=False,
+ dtype=wp.vec3,
+ )
+ wp.copy(src=u, dest=params)
+
+
+def unparametrize(params, vertices, M):
+ with torch.no_grad():
+ u = wp.from_torch(
+ from_differential(L=M, u=wp.to_torch(params, requires_grad=False)),
+ requires_grad=False,
+ dtype=wp.vec3,
+ )
+ wp.copy(src=u, dest=vertices)
+
+
+def as_mesh(scene_or_mesh):
+ """
+ Convert a possible scene to a mesh.
+
+ If conversion occurs, the returned mesh has only vertex and face data.
+ """
+ if isinstance(scene_or_mesh, trimesh.Scene):
+ if len(scene_or_mesh.geometry) == 0:
+ mesh = None # empty scene
+ else:
+ # we lose texture information here
+ mesh = trimesh.util.concatenate(
+ tuple(
+ trimesh.Trimesh(vertices=g.vertices, faces=g.faces)
+ for g in scene_or_mesh.geometry.values()
+ )
+ )
+ else:
+ assert isinstance(scene_or_mesh, trimesh.Trimesh)
+ mesh = scene_or_mesh
+ return mesh
+
+
+class Example:
+ def __init__(
+ self,
+ target_path,
+ degree=2,
+ resolution=10,
+ serendipity=False,
+ viscosity=2.0,
+ screening=1.0,
+ boundary_compliance=0.001,
+ sampling_coord=0.33,
+ smoothing=10.0,
+ ):
+ self._iter = 0
+
+ self._viscosity = viscosity
+ self._screening = screening
+
+ self._boundary_compliance = boundary_compliance
+
+ self._emission_value = 1.0
+ self._smoothing = smoothing
+ self._h = 0.5 / resolution # target edge length for remeshing
+
+ # resolution of sampling planees
+ sampling_res = 64 # 2 * resolution
+
+ sphere_rad = sampling_coord / 2.0 # radius of initial sphere
+
+ bc_order = (
+ 2 + 2 * degree
+ ) # quadrature order for integrating BC over mesh triangles
+
+ # Initial guess (sphere)
+ vertices, faces = icosphere(int(np.log2(resolution)))
+ vertices *= sphere_rad
+
+ # Target mesh (ellipse at different resolution)
+ target = as_mesh(trimesh.load(target_path))
+
+ vmin, vmax = np.min(target.vertices, axis=0), np.max(target.vertices, axis=0)
+ scale = sampling_coord / np.max(vmax - vmin)
+ target.vertices = target.vertices - (vmax + vmin) / 2 # Center mesh on origin
+ target.vertices = target.vertices * scale * 1.9
+
+ # sampling planes
+ X, Y = np.meshgrid(
+ np.linspace(-1, 1, sampling_res),
+ np.linspace(-1, 1, sampling_res),
+ )
+ Z = np.ones_like(X)
+
+ P0 = np.stack((X, Y, Z), axis=-1).reshape(-1, 3)
+ P1 = np.stack((X, Y, -Z), axis=-1).reshape(-1, 3)
+ P2 = np.stack((Z, X, Y), axis=-1).reshape(-1, 3)
+ P3 = np.stack((-Z, X, Y), axis=-1).reshape(-1, 3)
+ P4 = np.stack((Y, Z, X), axis=-1).reshape(-1, 3)
+ P5 = np.stack((Y, -Z, X), axis=-1).reshape(-1, 3)
+
+ P = np.vstack((P0, P1, P2, P3, P4, P5))
+ P[:, 0] *= 0.33 * sampling_coord + 0.67 * scale * (vmax[0] - vmin[0])
+ P[:, 1] *= 0.33 * sampling_coord + 0.67 * scale * (vmax[1] - vmin[1])
+ P[:, 2] *= 0.33 * sampling_coord + 0.67 * scale * (vmax[2] - vmin[2])
+
+ # per-face BC evaluation points
+ # define per-triangle quadrature for non-conforming boundary condition
+ coords, weights = fem.geometry.element.Triangle().instantiate_quadrature(
+ order=bc_order, family=fem.Polynomial.GAUSS_LEGENDRE
+ )
+
+ # Renderer
+ ps.register_surface_mesh("surface", vertices, faces)
+ ps.register_surface_mesh("target", target.vertices, target.faces)
+ ps.register_point_cloud("samples", points=P, radius=0.0025)
+
+ # Move to warp arrays
+
+ self._proj_pos = wp.array(P, dtype=wp.vec3)
+
+ self._qp_coords = wp.array(coords, dtype=wp.vec3)
+ self._qp_weights = wp.array(weights, dtype=float)
+
+ self._vertices = wp.array(vertices, dtype=wp.vec3, requires_grad=True)
+ self._faces = wp.array(faces, dtype=int)
+
+ self._target_vertices = wp.array(
+ target.vertices, dtype=wp.vec3, requires_grad=True
+ )
+ self._target_faces = wp.array(target.faces, dtype=int)
+
+ # Init sim
+
+ res = wp.vec3i(resolution, resolution, resolution)
+ self._geo = fem.Grid3D(
+ res=res,
+ bounds_lo=wp.vec3(-1.0, -1.0, -1.0),
+ bounds_hi=wp.vec3(1.0, 1.0, 1.0),
+ )
+
+ # Function space
+ element_basis = fem.ElementBasis.SERENDIPITY if serendipity else None
+ self._scalar_space = fem.make_polynomial_space(
+ self._geo, degree=degree, element_basis=element_basis
+ )
+
+ print(
+ f"Cell count: {self._geo.cell_count()}, total nodes: {self._scalar_space.node_count()}"
+ )
+
+ # Scalar field over our function space
+ self._scalar_field: fem.DiscreteField = self._scalar_space.make_field()
+ self._scalar_field.dof_values.requires_grad = True
+
+ self._emission_field: fem.DiscreteField = self._scalar_space.make_field()
+ self._emission_field.dof_values.fill_(self._emission_value)
+
+ self._init_constant_forms()
+ self._setup_target()
+
+ def _init_constant_forms(self):
+ geo = self._geo
+ domain = fem.Cells(geometry=geo)
+
+ # Hard Dirichlet BC on exterior boundary
+ boundary = fem.BoundarySides(geo)
+
+ bd_test = fem.make_test(space=self._scalar_space, domain=boundary)
+ bd_trial = fem.make_trial(space=self._scalar_space, domain=boundary)
+ self._bd_matrix = fem.integrate(
+ boundary_projector_form,
+ fields={"u": bd_trial, "v": bd_test},
+ nodal=True,
+ output_dtype=float,
+ )
+ fem.dirichlet.normalize_dirichlet_projector(self._bd_matrix)
+
+ # Diffusion form
+ self._test = fem.make_test(space=self._scalar_space, domain=domain)
+ self._trial = fem.make_trial(space=self._scalar_space, domain=domain)
+
+ self._poisson_matrix = fem.integrate(
+ screened_diffusion_form,
+ fields={"u": self._trial, "v": self._test},
+ values={"nu": self._viscosity, "sigma": self._screening},
+ output_dtype=float,
+ )
+
+ # Points at which solution is sampled
+ self._sample_pic = fem.PicQuadrature(
+ domain=self._test.domain, positions=self._proj_pos
+ )
+ self._sampled_values = wp.array(
+ dtype=float,
+ shape=(self._sample_pic.total_point_count()),
+ requires_grad=True,
+ )
+
+ def _evaluate_forward(
+ self, mesh_vertices, mesh_faces, solution_field, solution_samples, tape=None
+ ):
+ domain = self._test.domain
+
+ with_gradient = tape is not None
+ if not with_gradient:
+ tape = wp.Tape()
+
+ # Generate points over mesh triangles
+ point_shape = (mesh_faces.shape[0], self._qp_coords.shape[0])
+ points = wp.empty(point_shape, dtype=wp.vec3, requires_grad=True)
+ point_measures = wp.empty(point_shape, dtype=float, requires_grad=True)
+ with tape:
+ wp.launch(
+ gen_face_samples,
+ point_shape,
+ inputs=[
+ mesh_faces,
+ mesh_vertices,
+ self._qp_coords,
+ self._qp_weights,
+ points,
+ point_measures,
+ ],
+ )
+
+ points = points.flatten()
+ point_measures = point_measures.flatten()
+ pic = fem.PicQuadrature(
+ domain=domain,
+ positions=points,
+ measures=point_measures,
+ requires_grad=True,
+ )
+
+ # Integrate weak BC over points to get left- and-right-hand-side
+ pen_matrix = fem.integrate(
+ interior_penalty_form,
+ quadrature=pic,
+ fields={"u": self._trial, "v": self._test},
+ values={
+ "strength": 1.0 / self._boundary_compliance,
+ },
+ output_dtype=float,
+ )
+ lhs = pen_matrix
+ lhs += self._poisson_matrix
+
+ rhs = wp.array(
+ shape=self._scalar_space.node_count(),
+ dtype=wp.float32,
+ requires_grad=with_gradient,
+ )
+ with tape:
+ fem.integrate(
+ interior_penalty_form,
+ quadrature=pic,
+ fields={"u": self._emission_field, "v": self._test},
+ values={
+ "strength": 1.0 / self._boundary_compliance,
+ },
+ output=rhs,
+ )
+ fem.project_linear_system(lhs, rhs, self._bd_matrix, normalize_projector=False)
+
+ # CG solve
+ fem_example_utils.bsr_cg(
+ lhs, b=rhs, x=solution_field.dof_values, tol=1.0e-6, quiet=True
+ )
+
+ if with_gradient:
+ # Register CG for backward pass
+ def backward_cg():
+ fem_example_utils.bsr_cg(
+ lhs, b=solution_field.dof_values.grad, x=rhs.grad, quiet=True
+ )
+
+ tape.record_func(backward_cg, arrays=[solution_field.dof_values, rhs])
+
+ # Interpolate solution on sampling planes
+ with tape:
+ fem.interpolate(
+ sample_solution,
+ dest=solution_samples,
+ fields={"u": solution_field},
+ quadrature=self._sample_pic,
+ )
+
+ def _reset_optimizer(self):
+ self._vertices, self._faces = remesh(self._vertices, self._faces, h=self._h)
+
+ self._laplacian = largesteps_matrix(
+ self._vertices, self._faces, self._smoothing
+ )
+ self._vertices_param = wp.empty_like(self._vertices)
+ parametrize(self._vertices, self._vertices_param, self._laplacian)
+
+ self._adam = wp.optim.Adam(
+ params=[self._vertices_param], lr=0.005 * (0.99**self._iter)
+ )
+
+ def _setup_target(self):
+ target_field = self._scalar_space.make_field()
+ self._target_values = wp.array(
+ dtype=float, shape=(self._sample_pic.total_point_count())
+ )
+ self._evaluate_forward(
+ self._target_vertices, self._target_faces, target_field, self._target_values
+ )
+
+ ps.get_point_cloud("samples").add_scalar_quantity(
+ "target", self._target_values.numpy()
+ )
+
+ def step(self):
+ # Log2 schedule for remeshing
+ if (1 << int(np.round(np.log2(self._iter + 1)))) == self._iter + 1:
+ self._reset_optimizer()
+
+ tape = wp.Tape()
+
+ # Go from smoothing param space to 3d vertex space, and register same op on grad tape
+ unparametrize(self._vertices_param, self._vertices, self._laplacian)
+
+ def unparametrize_grad():
+ unparametrize(
+ self._vertices.grad, self._vertices_param.grad, self._laplacian
+ )
+
+ tape.record_func(
+ unparametrize_grad, arrays=[self._vertices_param, self._vertices]
+ )
+
+ # Solve diffusion eq on get value on sampling planes
+ self._evaluate_forward(
+ self._vertices,
+ self._faces,
+ self._scalar_field,
+ self._sampled_values,
+ tape=tape,
+ )
+
+ # Evaluate loss and do backprop step
+ loss = wp.zeros(shape=(1,), dtype=wp.float64, requires_grad=True)
+ with tape:
+ wp.launch(
+ loss_fn,
+ dim=self._sampled_values.shape[0],
+ inputs=[self._target_values, self._sampled_values, loss],
+ )
+
+ print(f"Loss at iteration {self._iter}: {loss.numpy()[0]}")
+
+ tape.backward(loss)
+ self._adam.step([self._vertices.grad])
+
+ # Zero-out gradients for next step
+ tape.zero()
+
+ self._iter += 1
+
+ def render(self):
+ ps.register_surface_mesh("surface", self._vertices.numpy(), self._faces.numpy())
+ ps.get_point_cloud("samples").add_scalar_quantity(
+ "value",
+ self._sampled_values.numpy(),
+ enabled=True if self._iter <= 1 else None,
+ )
+ ps.get_point_cloud("samples").add_scalar_quantity(
+ "diff",
+ np.abs(self._sampled_values.numpy() - self._target_values.numpy()),
+ )
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
+ )
+ parser.add_argument("target_path")
+ parser.add_argument(
+ "--device", type=str, default=None, help="Override the default Warp device."
+ )
+ parser.add_argument("--resolution", type=int, default=32, help="Grid resolution.")
+ parser.add_argument(
+ "--degree", type=int, default=2, help="Polynomial degree of shape functions."
+ )
+ parser.add_argument(
+ "--serendipity",
+ action="store_true",
+ default=True,
+ help="Use Serendipity basis functions.",
+ )
+ parser.add_argument(
+ "--viscosity", type=float, default=2.0, help="Fluid viscosity parameter."
+ )
+ parser.add_argument(
+ "--screening", type=float, default=1.0, help="Screening parameter."
+ )
+ parser.add_argument(
+ "--smoothing", type=float, default=10, help="Smoothing parameter."
+ )
+ parser.add_argument(
+ "--boundary_compliance",
+ type=float,
+ default=0.001,
+ help="Dirichlet boundary condition compliance.",
+ )
+ parser.add_argument(
+ "--num_iters", type=int, default=250, help="Number of iterations"
+ )
+
+ args = parser.parse_args()
+
+ ps.init()
+
+ with wp.ScopedDevice(args.device):
+ example = Example(
+ target_path=args.target_path,
+ degree=args.degree,
+ resolution=args.resolution,
+ serendipity=args.serendipity,
+ viscosity=args.viscosity,
+ screening=args.screening,
+ boundary_compliance=args.boundary_compliance,
+ smoothing=args.smoothing,
+ )
+
+ def ps_callback():
+ if example._iter > args.num_iters:
+ return
+
+ example.step()
+ example.render()
+
+ # pseudo turntable
+ t = 0.025 * example._iter
+ c = np.cos(t)
+ s = np.sin(t)
+ r = 1.5
+ ps.look_at(camera_location=(r * c, 0.25, r * s), target=(0.0, 0.0, 0.0))
+
+ ps.screenshot()
+
+ ps.set_ground_plane_mode("none")
+ ps.set_user_callback(ps_callback)
+
+ ps.show()
diff --git a/deps/vomp/vomp/fem/ground_truth_tests/usd_drop_cube.py b/deps/vomp/vomp/fem/ground_truth_tests/usd_drop_cube.py
new file mode 100644
index 0000000000000000000000000000000000000000..94e305e897617b1e2d15b5d10fb1a6491b1857ff
--- /dev/null
+++ b/deps/vomp/vomp/fem/ground_truth_tests/usd_drop_cube.py
@@ -0,0 +1,1105 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+import numpy as np
+import os
+import sys
+
+import warp as wp
+import warp.fem as fem
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+from vomp.fem.simulations.cube_fall import GroundCollidingSim
+
+
+from vomp.fem.fem_examples.mfem.collisions import CollisionHandler
+
+
+from vomp.fem.fem_examples.mfem.softbody_sim import run_softbody_sim
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM
+
+
+from vomp.fem.simulations.object_simulation import (
+ load_usd_mesh,
+ merge_meshes,
+ voxelize_mesh,
+ build_tet_mesh,
+)
+
+
+def compute_usd_bounds(file_path):
+ """Return axis-aligned bounding box (min, max) of all meshes inside a USD file.
+
+ Parameters
+ ----------
+ file_path : str
+ Path to a .usd or .usda file containing Mesh prims.
+
+ Returns
+ -------
+ tuple[np.ndarray, np.ndarray]
+ (min_bounds, max_bounds) each shape (3,), dtype float32
+ """
+ try:
+ from pxr import Usd, UsdGeom
+ except ImportError:
+ raise RuntimeError(
+ "pxr module not found. Please install the USD Python bindings (pip install usd-core)."
+ )
+
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"USD file '{file_path}' not found")
+
+ stage = Usd.Stage.Open(file_path)
+ if stage is None:
+ raise RuntimeError(f"Failed to open USD stage '{file_path}'")
+
+ bb_min = np.array([np.inf, np.inf, np.inf], dtype=np.float32)
+ bb_max = np.array([-np.inf, -np.inf, -np.inf], dtype=np.float32)
+
+ for prim in Usd.PrimRange(stage.GetPseudoRoot()):
+ if prim.IsA(UsdGeom.Mesh):
+ mesh = UsdGeom.Mesh(prim)
+ points_attr = mesh.GetPointsAttr()
+ if not points_attr.HasAuthoredValue():
+ continue
+ verts = points_attr.Get()
+ if verts is None:
+ continue
+ pts = np.asarray([(v[0], v[1], v[2]) for v in verts], dtype=np.float32)
+ bb_min = np.minimum(bb_min, pts.min(axis=0))
+ bb_max = np.maximum(bb_max, pts.max(axis=0))
+
+ if np.any(bb_min == np.inf):
+ raise RuntimeError("No Mesh prims with point data found in USD file")
+
+ return bb_min, bb_max
+
+
+def build_simulation_grid(tet_vertices: np.ndarray, res: int, pad_ratio: float = 0.05):
+ """Return a Grid3D covering the tetrahedral mesh plus padding.
+
+ Also returns the bounds (lo, hi) arrays.
+ """
+ min_bounds = tet_vertices.min(axis=0)
+ max_bounds = tet_vertices.max(axis=0)
+
+ pad = (max_bounds - min_bounds) * pad_ratio
+ min_bounds -= pad
+ max_bounds += pad
+
+ grid = fem.Grid3D(
+ res=wp.vec3i(res, res, res),
+ bounds_lo=wp.vec3(*min_bounds),
+ bounds_hi=wp.vec3(*max_bounds),
+ )
+ return grid, min_bounds, max_bounds
+
+
+class ObjectCollidingSim(GroundCollidingSim):
+ """Extends GroundCollidingSim to handle collisions with kinematic objects."""
+
+ def __init__(self, geo, active_cells, args, kinematic_meshes=None):
+ super().__init__(geo, active_cells, args)
+ self.kinematic_meshes = kinematic_meshes or []
+
+ def init_collision_detector(self):
+ """Create a CollisionHandler with both ground and object collision support."""
+
+ node_pos = self.u_field.space.node_positions()
+
+ pic_qp = fem.PicQuadrature(fem.Cells(self.geo), node_pos)
+ pic_qp.domain = self.u_test.domain
+
+ self.collision_handler = CollisionHandler(
+ kinematic_meshes=self.kinematic_meshes,
+ cp_cell_indices=pic_qp.cell_indices,
+ cp_cell_coords=pic_qp.particle_coords,
+ sim=self,
+ )
+
+ self._cp_quadrature = pic_qp
+
+
+def create_dropping_cube_mesh(
+ center, size, time_step, current_frame, drop_height=2.0, drop_speed=2.0
+):
+ """Create a warp mesh for a cube that drops from above.
+
+ Parameters:
+ -----------
+ center : tuple
+ (x, z) center position of the cube
+ size : float
+ Side length of the cube
+ time_step : float
+ Simulation time step
+ current_frame : int
+ Current frame number
+ drop_height : float
+ Initial height above the object
+ drop_speed : float
+ Falling speed
+
+ Returns:
+ --------
+ wp.Mesh : A warp mesh representing the cube
+ """
+ half_size = size / 2.0
+
+ current_time = current_frame * time_step
+ y_pos = drop_height - drop_speed * current_time
+ y_pos = max(y_pos, half_size)
+
+ vertices = np.array(
+ [
+ [center[0] - half_size, y_pos - half_size, center[1] - half_size],
+ [center[0] + half_size, y_pos - half_size, center[1] - half_size],
+ [center[0] - half_size, y_pos + half_size, center[1] - half_size],
+ [center[0] + half_size, y_pos + half_size, center[1] - half_size],
+ [center[0] - half_size, y_pos - half_size, center[1] + half_size],
+ [center[0] + half_size, y_pos - half_size, center[1] + half_size],
+ [center[0] - half_size, y_pos + half_size, center[1] + half_size],
+ [center[0] + half_size, y_pos + half_size, center[1] + half_size],
+ ],
+ dtype=np.float32,
+ )
+
+ indices = np.array(
+ [
+ [0, 2, 1],
+ [1, 2, 3],
+ [4, 5, 6],
+ [5, 7, 6],
+ [0, 1, 4],
+ [1, 5, 4],
+ [2, 6, 3],
+ [3, 6, 7],
+ [0, 4, 2],
+ [2, 4, 6],
+ [1, 3, 5],
+ [3, 7, 5],
+ ],
+ dtype=np.int32,
+ ).flatten()
+
+ velocities = np.zeros_like(vertices)
+ if y_pos > half_size:
+ velocities[:, 1] = -drop_speed
+
+ mesh = wp.Mesh(
+ points=wp.array(vertices, dtype=wp.vec3),
+ indices=wp.array(indices, dtype=int),
+ velocities=wp.array(velocities, dtype=wp.vec3),
+ )
+
+ return mesh
+
+
+def create_cube_tetrahedra(vertices):
+ """Create tetrahedral elements for a cube given its 8 vertices.
+
+ Parameters:
+ -----------
+ vertices : np.ndarray
+ Array of shape (8, 3) containing the cube vertices
+
+ Returns:
+ --------
+ np.ndarray : Array of shape (n_tets, 4) containing tetrahedral connectivity
+ """
+ # Subdivide cube into 5 tetrahedra
+ # This is a standard subdivision that ensures compatibility
+ tets = np.array(
+ [[0, 1, 2, 4], [1, 3, 2, 7], [1, 5, 4, 7], [2, 6, 4, 7], [1, 2, 4, 7]],
+ dtype=np.int32,
+ )
+
+ return tets
+
+
+def get_cube_position(
+ center, size, time_step, current_frame, drop_height=2.0, drop_speed=2.0
+):
+ """Calculate the current position of the cube.
+
+ Returns:
+ --------
+ tuple : (y_position, vertices_array)
+ """
+ half_size = size / 2.0
+
+ current_time = current_frame * time_step
+ y_pos = drop_height - drop_speed * current_time
+ y_pos = max(y_pos, half_size)
+
+ vertices = np.array(
+ [
+ [center[0] - half_size, y_pos - half_size, center[1] - half_size],
+ [center[0] + half_size, y_pos - half_size, center[1] - half_size],
+ [center[0] - half_size, y_pos + half_size, center[1] - half_size],
+ [center[0] + half_size, y_pos + half_size, center[1] - half_size],
+ [center[0] - half_size, y_pos - half_size, center[1] + half_size],
+ [center[0] + half_size, y_pos - half_size, center[1] + half_size],
+ [center[0] - half_size, y_pos + half_size, center[1] + half_size],
+ [center[0] + half_size, y_pos + half_size, center[1] + half_size],
+ ],
+ dtype=np.float32,
+ )
+
+ return y_pos, vertices
+
+
+def update_cube_mesh_position(
+ mesh, center, size, time_step, current_frame, drop_height=2.0, drop_speed=2.0
+):
+ """Update the position of an existing cube mesh.
+
+ Parameters:
+ -----------
+ mesh : wp.Mesh
+ The mesh to update
+ center : tuple
+ (x, z) center position of the cube
+ size : float
+ Side length of the cube
+ time_step : float
+ Simulation time step
+ current_frame : int
+ Current frame number
+ drop_height : float
+ Initial height above the object
+ drop_speed : float
+ Falling speed
+ """
+ half_size = size / 2.0
+
+ current_time = current_frame * time_step
+ y_pos = drop_height - drop_speed * current_time
+ y_pos = max(y_pos, half_size)
+
+ vertices = np.array(
+ [
+ [center[0] - half_size, y_pos - half_size, center[1] - half_size],
+ [center[0] + half_size, y_pos - half_size, center[1] - half_size],
+ [center[0] - half_size, y_pos + half_size, center[1] - half_size],
+ [center[0] + half_size, y_pos + half_size, center[1] - half_size],
+ [center[0] - half_size, y_pos - half_size, center[1] + half_size],
+ [center[0] + half_size, y_pos - half_size, center[1] + half_size],
+ [center[0] - half_size, y_pos + half_size, center[1] + half_size],
+ [center[0] + half_size, y_pos + half_size, center[1] + half_size],
+ ],
+ dtype=np.float32,
+ )
+
+ velocities = np.zeros_like(vertices)
+ if y_pos > half_size:
+ velocities[:, 1] = -drop_speed
+
+ wp.copy(mesh.points, wp.array(vertices, dtype=wp.vec3))
+ wp.copy(mesh.velocities, wp.array(velocities, dtype=wp.vec3))
+
+ mesh.refit()
+
+
+class MultiObjectSim:
+ """Manages multiple FEM objects with collision detection between them."""
+
+ def __init__(self, args):
+ self.args = args
+ self.sims = []
+ self.meshes = []
+ self.kinematic_meshes = []
+
+ def add_fem_object(
+ self,
+ tet_vertices,
+ tet_elements,
+ active_cells,
+ young_modulus,
+ poisson_ratio,
+ density,
+ initial_velocity=None,
+ material_map=None,
+ ):
+ """Add a FEM object to the simulation.
+
+ Args:
+ material_map: Optional dict mapping cell indices to (young_modulus, poisson_ratio) tuples
+ """
+ # Create grid for this object
+ geo, bounds_lo, bounds_hi = build_simulation_grid(
+ tet_vertices, self.args.resolution
+ )
+
+ # Create simulation for this object
+ sim = GroundCollidingSim(
+ geo,
+ wp.array(active_cells, dtype=wp.int32),
+ self.args,
+ )
+
+ # Set default material properties
+ sim.args.young_modulus = young_modulus
+ sim.args.poisson_ratio = poisson_ratio
+ sim.args.density = density
+
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ # Apply material map if provided
+ if material_map is not None:
+
+ def lame_from_E_nu(E, nu):
+ lam = E * nu / ((1 + nu) * (1 - 2 * nu))
+ mu = E / (2 * (1 + nu))
+ return lam, mu
+
+ # Default Lame parameters
+ lam0, mu0 = lame_from_E_nu(young_modulus, poisson_ratio)
+ lame_np = np.full((geo.cell_count(), 2), [lam0, mu0], dtype=np.float32)
+
+ # Apply specific materials to cells
+ for cell_idx, (E, nu) in material_map.items():
+ if cell_idx < geo.cell_count():
+ lam, mu = lame_from_E_nu(E, nu)
+ lame_np[cell_idx] = [lam, mu]
+
+ sim.lame_field.dof_values.assign(wp.array(lame_np, dtype=wp.vec2))
+
+ # Store initial positions
+ sim.initial_positions = sim.u_field.space.node_positions().numpy().copy()
+
+ # Set initial velocity if provided
+ if initial_velocity is not None:
+ node_positions = sim.u_field.space.node_positions().numpy()
+ velocities = np.zeros_like(node_positions)
+
+ # Apply velocity to all nodes
+ velocities[:] = initial_velocity
+
+ if hasattr(sim, "v_field"):
+ sim.v_field.dof_values.assign(wp.array(velocities, dtype=wp.vec3))
+
+ self.sims.append(sim)
+
+ # Create mesh for collision detection
+ mesh_points = wp.array(tet_vertices, dtype=wp.vec3)
+ mesh_velocities = wp.zeros_like(mesh_points)
+
+ # Simple cube faces for collision mesh
+ if len(tet_vertices) >= 8:
+ faces = np.array(
+ [
+ [0, 2, 1],
+ [1, 2, 3],
+ [4, 5, 6],
+ [5, 7, 6],
+ [0, 1, 4],
+ [1, 5, 4],
+ [2, 6, 3],
+ [3, 6, 7],
+ [0, 4, 2],
+ [2, 4, 6],
+ [1, 3, 5],
+ [3, 7, 5],
+ ],
+ dtype=np.int32,
+ ).flatten()
+ else:
+ faces = np.array([0, 1, 2], dtype=np.int32) # Dummy face
+
+ mesh = wp.Mesh(
+ points=mesh_points,
+ indices=wp.array(faces, dtype=int),
+ velocities=mesh_velocities,
+ )
+
+ self.meshes.append(mesh)
+
+ return len(self.sims) - 1
+
+ def init_simulations(self):
+ """Initialize all simulations with collision detection."""
+ for i, sim in enumerate(self.sims):
+ # Each sim sees other sims' meshes as kinematic objects
+ other_meshes = [self.meshes[j] for j in range(len(self.meshes)) if j != i]
+
+ sim.init_collision_detector()
+
+ # If the sim has collision handler, update it with other meshes
+ if hasattr(sim, "collision_handler") and hasattr(
+ sim.collision_handler, "kinematic_meshes"
+ ):
+ sim.collision_handler.kinematic_meshes = other_meshes
+
+ sim.set_boundary_condition(boundary_projector_form=None)
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ def update_collision_meshes(self):
+ """Update collision meshes based on current deformations."""
+ for i, (sim, mesh) in enumerate(zip(self.sims, self.meshes)):
+ # Get current positions
+ positions = sim.u_field.space.node_positions().numpy()
+ displacements = sim.u_field.dof_values.numpy()
+ current_positions = positions + displacements
+
+ # Update mesh points (only first 8 vertices for cube)
+ n_update = min(8, len(current_positions))
+ mesh.points.numpy()[:n_update] = current_positions[:n_update]
+
+ # Update velocities if available
+ if hasattr(sim, "v_field"):
+ velocities = sim.v_field.dof_values.numpy()
+ mesh.velocities.numpy()[:n_update] = velocities[:n_update]
+
+ mesh.refit()
+
+ def run_frame(self):
+ """Run one frame of simulation for all objects."""
+ # Update collision meshes before running physics
+ self.update_collision_meshes()
+
+ # Run physics for each object
+ for sim in self.sims:
+ sim.run_frame()
+
+ def get_positions(self):
+ """Get current positions for all objects."""
+ all_positions = []
+ for sim in self.sims:
+ positions = sim.u_field.space.node_positions().numpy()
+ displacements = sim.u_field.dof_values.numpy()
+ current_positions = positions + displacements
+ all_positions.append(current_positions)
+ return all_positions
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser(description="Simple drop simulation of a USD mesh")
+ parser.add_argument(
+ "--usd_file",
+ type=str,
+ required=True,
+ help="Path to the USD file to drop",
+ )
+ parser.add_argument(
+ "--resolution",
+ type=int,
+ default=20,
+ help="Grid resolution per axis for the FEM discretisation",
+ )
+ parser.add_argument(
+ "--voxel_size",
+ type=float,
+ default=0.03,
+ help="Size of voxels used to approximate the USD mesh",
+ )
+ parser.add_argument(
+ "--ui",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Enable Polyscope visualisation",
+ )
+
+ parser.add_argument(
+ "--youngs",
+ type=float,
+ nargs="+",
+ default=[1e4],
+ help="Young's modulus per mesh in order",
+ )
+ parser.add_argument(
+ "--poissons",
+ type=float,
+ nargs="+",
+ default=[0.45],
+ help="Poisson ratio per mesh in order",
+ )
+ parser.add_argument(
+ "--densities",
+ type=float,
+ nargs="+",
+ default=[500.0],
+ help="Density per mesh in order (not used yet)",
+ )
+
+ GroundCollidingSim.add_parser_arguments(parser)
+
+ parser.add_argument(
+ "--cube_size",
+ type=float,
+ default=0.3,
+ help="Size of the dropping cube",
+ )
+ parser.add_argument(
+ "--cube_drop_height",
+ type=float,
+ default=1.2,
+ help="Initial height of the cube above the object",
+ )
+ parser.add_argument(
+ "--cube_drop_speed",
+ type=float,
+ default=1.5,
+ help="Falling speed of the cube",
+ )
+ parser.add_argument(
+ "--cube_x",
+ type=float,
+ default=0.0,
+ help="X position of the cube center",
+ )
+ parser.add_argument(
+ "--cube_z",
+ type=float,
+ default=0.0,
+ help="Z position of the cube center",
+ )
+ parser.add_argument(
+ "--cube_young",
+ type=float,
+ default=5e3,
+ help="Young's modulus of the cube",
+ )
+ parser.add_argument(
+ "--cube_poisson",
+ type=float,
+ default=0.3,
+ help="Poisson ratio of the cube",
+ )
+ parser.add_argument(
+ "--cube_density",
+ type=float,
+ default=1000.0,
+ help="Density of the cube",
+ )
+
+ parser.set_defaults(
+ n_newton=10,
+ n_frames=250,
+ young_modulus=1e4,
+ poisson_ratio=0.45,
+ density=500.0,
+ gravity=9.81,
+ ground=True,
+ ground_height=0.0,
+ collision_radius=0.01,
+ dt=0.05,
+ )
+
+ args = parser.parse_args()
+
+ usd_meshes = load_usd_mesh(args.usd_file)
+
+ print("Meshes found (index : name):")
+ for idx, (_, _, name) in enumerate(usd_meshes):
+ print(f" [{idx}] {name}")
+
+ n_mesh = len(usd_meshes)
+
+ def _match_list(lst, fill):
+ if len(lst) >= n_mesh:
+ return lst[:n_mesh]
+ else:
+ return lst + [fill] * (n_mesh - len(lst))
+
+ young_list = _match_list(args.youngs, args.youngs[-1])
+ pois_list = _match_list(args.poissons, args.poissons[-1])
+ dens_list = _match_list(args.densities, args.densities[-1])
+
+ print("\nMaterial properties assigned:")
+ for idx, (_, _, name) in enumerate(usd_meshes):
+ if idx < len(young_list):
+ print(f" [{idx}] {name}: E={young_list[idx]:.2e} Pa, nu={pois_list[idx]}")
+
+ voxel_centers_all = []
+ mesh_indices = []
+
+ for m_idx, (vtx, faces, _name) in enumerate(usd_meshes):
+ vc, _ = voxelize_mesh(vtx, faces, voxel_size=args.voxel_size)
+ if vc is not None and len(vc):
+ voxel_centers_all.append(vc)
+ mesh_indices.append(np.full(len(vc), m_idx, dtype=np.int32))
+
+ if not voxel_centers_all:
+ raise RuntimeError("Voxelisation produced no voxels โ cannot proceed")
+
+ voxel_centers = np.concatenate(voxel_centers_all, axis=0)
+ voxel_mesh_idx = np.concatenate(mesh_indices, axis=0)
+
+ voxel_centers = voxel_centers[:, [0, 2, 1]]
+ voxel_centers[:, 2] *= -1.0
+
+ min_y = voxel_centers[:, 1].min()
+ lift_amount = 0.5 - min_y
+ voxel_centers[:, 1] += lift_amount
+
+ # Create voxels for the cube
+ cube_center = (args.cube_x, args.cube_z)
+ cube_voxel_size = (
+ args.voxel_size
+ ) # Use same voxel size as the object for consistency
+
+ # Calculate initial cube position
+ initial_cube_y = args.cube_drop_height
+
+ # Ensure cube starts above the object with minimum separation
+ object_max_y = voxel_centers[:, 1].max()
+ min_separation = 0.2 # Minimum 20cm separation
+ min_cube_y = object_max_y + args.cube_size / 2.0 + min_separation
+
+ if initial_cube_y < min_cube_y:
+ print(
+ f"Warning: Adjusting cube height from {initial_cube_y:.3f} to {min_cube_y:.3f} to ensure separation"
+ )
+ initial_cube_y = min_cube_y
+
+ # Generate cube voxel centers
+ cube_voxels = []
+ half_size = args.cube_size / 2.0
+ n_voxels_per_side = max(2, int(args.cube_size / cube_voxel_size))
+ actual_voxel_size = args.cube_size / n_voxels_per_side
+
+ for i in range(n_voxels_per_side):
+ for j in range(n_voxels_per_side):
+ for k in range(n_voxels_per_side):
+ x = cube_center[0] - half_size + (i + 0.5) * actual_voxel_size
+ y = initial_cube_y - half_size + (j + 0.5) * actual_voxel_size
+ z = cube_center[1] - half_size + (k + 0.5) * actual_voxel_size
+ cube_voxels.append([x, y, z])
+
+ cube_voxels = np.array(cube_voxels, dtype=np.float32)
+ n_cube_voxels = len(cube_voxels)
+
+ # Combine object and cube voxels
+ all_voxel_centers = np.vstack([voxel_centers, cube_voxels])
+
+ # Create mesh indices for the cube (use the next index after existing meshes)
+ cube_mesh_idx = n_mesh
+ all_voxel_mesh_idx = np.concatenate(
+ [voxel_mesh_idx, np.full(n_cube_voxels, cube_mesh_idx, dtype=np.int32)]
+ )
+
+ # Build tetrahedral meshes separately to avoid connections
+ object_tet_vertices, object_tet_elements = build_tet_mesh(
+ voxel_centers, args.voxel_size
+ )
+ cube_tet_vertices_local, cube_tet_elements_local = build_tet_mesh(
+ cube_voxels, args.voxel_size
+ )
+
+ # Create multi-object simulation
+ multi_sim = MultiObjectSim(args)
+
+ # Create material map for object based on voxel mesh indices
+ object_material_map = {}
+
+ # Map voxels to cells in the object grid
+ object_geo, bounds_lo, bounds_hi = build_simulation_grid(
+ object_tet_vertices, args.resolution
+ )
+ res = args.resolution
+
+ # Convert bounds to numpy arrays
+ bounds_lo_np = np.array(
+ [bounds_lo[0], bounds_lo[1], bounds_lo[2]], dtype=np.float32
+ )
+ bounds_hi_np = np.array(
+ [bounds_hi[0], bounds_hi[1], bounds_hi[2]], dtype=np.float32
+ )
+
+ for v_idx, v in enumerate(object_tet_vertices):
+ # Calculate cell index for this vertex
+ rel = (v - bounds_lo_np) / (bounds_hi_np - bounds_lo_np)
+ cx = min(res - 1, max(0, int(rel[0] * res)))
+ cy = min(res - 1, max(0, int(rel[1] * res)))
+ cz = min(res - 1, max(0, int(rel[2] * res)))
+ cell_idx = cx + cy * res + cz * res * res
+
+ # Get mesh index for this vertex
+ voxel_idx = v_idx // 8
+ if voxel_idx < len(voxel_mesh_idx):
+ m_idx = voxel_mesh_idx[voxel_idx]
+ if m_idx < len(young_list):
+ E = young_list[m_idx]
+ nu = pois_list[m_idx]
+ object_material_map[cell_idx] = (E, nu)
+
+ print(
+ f"Material map created with {len(object_material_map)} unique cell assignments"
+ )
+ for m_idx in range(len(young_list)):
+ count = sum(
+ 1 for _, (E, _) in object_material_map.items() if E == young_list[m_idx]
+ )
+ print(f" Mesh {m_idx}: {count} cells with E={young_list[m_idx]:.2e}")
+
+ # Add object to simulation
+ object_cells = np.arange(len(voxel_centers) * 8) # 8 cells per voxel
+ object_idx = multi_sim.add_fem_object(
+ object_tet_vertices,
+ object_tet_elements,
+ object_cells,
+ young_modulus=young_list[0],
+ poisson_ratio=pois_list[0],
+ density=dens_list[0],
+ initial_velocity=None, # Object starts at rest
+ material_map=object_material_map,
+ )
+
+ # Add cube to simulation
+ cube_cells = np.arange(len(cube_voxels) * 8)
+ cube_initial_velocity = np.array([0.0, -args.cube_drop_speed, 0.0])
+ cube_idx = multi_sim.add_fem_object(
+ cube_tet_vertices_local,
+ cube_tet_elements_local,
+ cube_cells,
+ young_modulus=args.cube_young,
+ poisson_ratio=args.cube_poisson,
+ density=args.cube_density,
+ initial_velocity=cube_initial_velocity,
+ material_map=None, # No material map for cube
+ )
+
+ # Initialize all simulations with collision detection
+ multi_sim.init_simulations()
+
+ print(
+ f"Object simulation: {len(object_tet_vertices)} vertices, {len(object_tet_elements)} tets"
+ )
+ print(
+ f"Cube simulation: {len(cube_tet_vertices_local)} vertices, {len(cube_tet_elements_local)} tets"
+ )
+
+ # For compatibility with visualization, combine vertices
+ tet_vertices = np.vstack([object_tet_vertices, cube_tet_vertices_local])
+ tet_elements = np.vstack(
+ [object_tet_elements, cube_tet_elements_local + len(object_tet_vertices)]
+ )
+
+ n_object_vertices = len(object_tet_vertices)
+ n_cube_vertices = len(cube_tet_vertices_local)
+
+ # Debug: Check initial positions
+ print(
+ f"Object voxels Y range: {voxel_centers[:, 1].min():.3f} to {voxel_centers[:, 1].max():.3f}"
+ )
+ print(
+ f"Cube voxels Y range: {cube_voxels[:, 1].min():.3f} to {cube_voxels[:, 1].max():.3f}"
+ )
+ print(
+ f"Initial separation: {cube_voxels[:, 1].min() - voxel_centers[:, 1].max():.3f}"
+ )
+
+ merged_all_vertices, merged_all_faces = merge_meshes(usd_meshes)
+
+ surf_vertices = merged_all_vertices.copy()
+
+ rot_surf = surf_vertices.copy()
+ rot_surf[:, 1], rot_surf[:, 2] = surf_vertices[:, 2], -surf_vertices[:, 1]
+ rot_surf[:, 1] += lift_amount
+
+ surf_vertices = rot_surf
+ surf_faces = merged_all_faces.astype(np.int32)
+
+ max_valid = surf_vertices.shape[0] - 1
+ surf_faces = np.where(surf_faces > max_valid, max_valid, surf_faces)
+
+ recorded = []
+
+ # Record initial positions from both simulations
+ initial_positions = multi_sim.get_positions()
+ all_initial_pos = np.vstack(initial_positions)
+ recorded.append(all_initial_pos)
+
+ for frame in range(args.n_frames):
+
+ multi_sim.run_frame()
+
+ # Get positions from all objects
+ current_positions = multi_sim.get_positions()
+ all_positions = np.vstack(current_positions)
+ recorded.append(all_positions.copy())
+
+ # Calculate total energy
+ total_energy = 0.0
+ for i, sim in enumerate(multi_sim.sims):
+ energy = float(sim.evaluate_energy()[0])
+ total_energy += energy
+
+ if frame % 10 == 0:
+ print(f"Frame {frame+1}/{args.n_frames} total energy: {total_energy:.2e}")
+
+ if not args.ui:
+ return
+
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ ps.init()
+ ps.set_ground_plane_mode("tile")
+ ps.set_ground_plane_height(0.0)
+
+ surf_mesh = ps.register_surface_mesh(
+ "usd_mesh",
+ surf_vertices,
+ surf_faces,
+ edge_width=1.0,
+ )
+ surf_mesh.set_smooth_shade(True)
+ surf_mesh.set_edge_width(0.5)
+ surf_mesh.set_edge_color((0.1, 0.1, 0.1))
+
+ # Create cube surface mesh vertices and faces
+ cube_surf_vertices = cube_tet_vertices_local[
+ :8
+ ].copy() # First 8 vertices form the cube
+ cube_surf_faces = np.array(
+ [
+ [0, 2, 1],
+ [1, 2, 3], # Bottom
+ [4, 5, 6],
+ [5, 7, 6], # Top
+ [0, 1, 4],
+ [1, 5, 4], # Front
+ [2, 6, 3],
+ [3, 6, 7], # Back
+ [0, 4, 2],
+ [2, 4, 6], # Left
+ [1, 3, 5],
+ [3, 7, 5], # Right
+ ],
+ dtype=np.int32,
+ )
+
+ # Find the actual cube surface vertices by looking for vertices in the cube region
+ cube_vertex_mask = (
+ (cube_tet_vertices_local[:, 0] >= cube_center[0] - args.cube_size / 2 - 0.05)
+ & (cube_tet_vertices_local[:, 0] <= cube_center[0] + args.cube_size / 2 + 0.05)
+ & (cube_tet_vertices_local[:, 1] >= initial_cube_y - args.cube_size / 2 - 0.05)
+ & (cube_tet_vertices_local[:, 1] <= initial_cube_y + args.cube_size / 2 + 0.05)
+ & (cube_tet_vertices_local[:, 2] >= cube_center[1] - args.cube_size / 2 - 0.05)
+ & (cube_tet_vertices_local[:, 2] <= cube_center[1] + args.cube_size / 2 + 0.05)
+ )
+
+ # Get the outer vertices of the cube
+ cube_region_vertices = cube_tet_vertices_local[cube_vertex_mask]
+ if len(cube_region_vertices) >= 8:
+ # Find the 8 corner vertices
+ min_x, max_x = (
+ cube_region_vertices[:, 0].min(),
+ cube_region_vertices[:, 0].max(),
+ )
+ min_y, max_y = (
+ cube_region_vertices[:, 1].min(),
+ cube_region_vertices[:, 1].max(),
+ )
+ min_z, max_z = (
+ cube_region_vertices[:, 2].min(),
+ cube_region_vertices[:, 2].max(),
+ )
+
+ # Create the 8 corner vertices
+ cube_surf_vertices = np.array(
+ [
+ [min_x, min_y, min_z], # 0
+ [max_x, min_y, min_z], # 1
+ [min_x, max_y, min_z], # 2
+ [max_x, max_y, min_z], # 3
+ [min_x, min_y, max_z], # 4
+ [max_x, min_y, max_z], # 5
+ [min_x, max_y, max_z], # 6
+ [max_x, max_y, max_z], # 7
+ ],
+ dtype=np.float32,
+ )
+ else:
+ # Fallback: use the cube tet vertices directly
+ print(f"Warning: Using first 8 cube tet vertices for surface mesh")
+ cube_surf_vertices = cube_tet_vertices_local[:8].copy()
+
+ # Register cube surface mesh
+ cube_surf_mesh = ps.register_surface_mesh(
+ "cube_surface",
+ cube_surf_vertices,
+ cube_surf_faces,
+ edge_width=1.0,
+ )
+ cube_surf_mesh.set_smooth_shade(True)
+ cube_surf_mesh.set_color((1.0, 0.2, 0.2))
+ cube_surf_mesh.set_edge_width(0.5)
+ cube_surf_mesh.set_edge_color((0.5, 0.1, 0.1))
+
+ # Register object tetrahedral mesh
+ object_mesh = ps.register_volume_mesh(
+ "object_tet",
+ object_tet_vertices,
+ tets=object_tet_elements,
+ edge_width=1.0,
+ )
+
+ # Enable the physics mesh and make it semi-transparent
+ object_mesh.set_enabled(True)
+ object_mesh.set_transparency(0.3)
+ object_mesh.set_color((0.2, 0.8, 0.2)) # Green color for physics mesh
+ object_mesh.set_edge_width(2.0)
+ object_mesh.set_edge_color((0.0, 0.5, 0.0))
+
+ # Register cube tetrahedral mesh
+ cube_tet_mesh = ps.register_volume_mesh(
+ "cube_physics",
+ cube_tet_vertices_local,
+ tets=cube_tet_elements_local,
+ edge_width=1.0,
+ )
+ cube_tet_mesh.set_enabled(True)
+ cube_tet_mesh.set_transparency(0.3)
+ cube_tet_mesh.set_color((0.8, 0.2, 0.2)) # Red color for cube physics mesh
+ cube_tet_mesh.set_edge_width(2.0)
+ cube_tet_mesh.set_edge_color((0.5, 0.0, 0.0))
+
+ from scipy.spatial import KDTree
+
+ kdtree = KDTree(recorded[0])
+
+ # Map object tet vertices to simulation nodes
+ object_tet_to_node = [kdtree.query(v)[1] for v in object_tet_vertices]
+ cube_tet_to_node = [kdtree.query(v)[1] for v in cube_tet_vertices_local]
+ surf_to_node = [kdtree.query(v)[1] for v in surf_vertices]
+
+ # Map cube surface vertices to nodes
+ cube_surf_to_node = [kdtree.query(v)[1] for v in cube_surf_vertices]
+
+ current = [0]
+ play = [False]
+ last = [time.time()]
+ fps = [20]
+ show_physics_mesh = [True]
+ show_surface_mesh = [True]
+ show_cube_physics = [True]
+ show_cube_surface = [True]
+
+ def _ui():
+ psim.Text("Mesh Visibility")
+
+ psim.Text("Object Meshes:")
+ changed_physics, val_physics = psim.Checkbox(
+ "Show Object Physics Mesh", show_physics_mesh[0]
+ )
+ if changed_physics:
+ show_physics_mesh[0] = val_physics
+ object_mesh.set_enabled(val_physics)
+
+ changed_surface, val_surface = psim.Checkbox(
+ "Show Object Surface Mesh", show_surface_mesh[0]
+ )
+ if changed_surface:
+ show_surface_mesh[0] = val_surface
+ surf_mesh.set_enabled(val_surface)
+
+ psim.Text("Cube Meshes:")
+ changed_cube_physics, val_cube_physics = psim.Checkbox(
+ "Show Cube Physics Mesh", show_cube_physics[0]
+ )
+ if changed_cube_physics:
+ show_cube_physics[0] = val_cube_physics
+ cube_tet_mesh.set_enabled(val_cube_physics)
+
+ changed_cube_surface, val_cube_surface = psim.Checkbox(
+ "Show Cube Surface Mesh", show_cube_surface[0]
+ )
+ if changed_cube_surface:
+ show_cube_surface[0] = val_cube_surface
+ cube_surf_mesh.set_enabled(val_cube_surface)
+
+ psim.Separator()
+ psim.Text("Transparency Settings")
+
+ if show_physics_mesh[0]:
+ changed_transparency, val_transparency = psim.SliderFloat(
+ "Object Physics Mesh Transparency",
+ object_mesh.get_transparency(),
+ 0.0,
+ 1.0,
+ )
+ if changed_transparency:
+ object_mesh.set_transparency(val_transparency)
+
+ if show_cube_physics[0]:
+ changed_cube_transparency, val_cube_transparency = psim.SliderFloat(
+ "Cube Physics Mesh Transparency",
+ cube_tet_mesh.get_transparency(),
+ 0.0,
+ 1.0,
+ )
+ if changed_cube_transparency:
+ cube_tet_mesh.set_transparency(val_cube_transparency)
+
+ psim.Separator()
+ psim.Text("Animation Controls")
+
+ changed, val = psim.SliderInt("frame", current[0], 0, len(recorded) - 1)
+ if changed:
+ current[0] = val
+ _update_frame(val)
+
+ changed_fps, new_fps = psim.SliderInt("fps", fps[0], 1, 60)
+ if changed_fps:
+ fps[0] = new_fps
+
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last[0] = time.time()
+
+ if play[0] and time.time() - last[0] > 1.0 / fps[0]:
+ current[0] = (current[0] + 1) % len(recorded)
+ _update_frame(current[0])
+ last[0] = time.time()
+
+ def _update_frame(idx):
+ disp = recorded[idx] - recorded[0]
+
+ # Update object tetrahedral mesh
+ object_mesh.update_vertex_positions(
+ object_tet_vertices + disp[object_tet_to_node]
+ )
+
+ # Update cube tetrahedral mesh
+ cube_tet_mesh.update_vertex_positions(
+ cube_tet_vertices_local + disp[cube_tet_to_node]
+ )
+
+ # Update cube surface mesh
+ cube_surf_mesh.update_vertex_positions(
+ cube_surf_vertices + disp[cube_surf_to_node]
+ )
+
+ # Update surface mesh
+ surf_mesh.update_vertex_positions(surf_vertices + disp[surf_to_node])
+
+ ps.set_user_callback(_ui)
+ ps.show()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/scripts/example_cutting/README.md b/deps/vomp/vomp/fem/scripts/example_cutting/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9ec478c120c12ed02eb14e99cdefdcf8349f9b9a
--- /dev/null
+++ b/deps/vomp/vomp/fem/scripts/example_cutting/README.md
@@ -0,0 +1,28 @@
+# Topo cutting example
+
+
+## Requirements
+
+The following additional Python packages must be installed
+
+ - polyscope==2.1.*
+ - [kaolin](https://github.com/NVIDIAGameWorks/kaolin)
+
+This example depends on the NVIDIA kaolin FlexiCubes implementation, which is part of the namespace `kaolin.non_commercial` released under the [NSCL license](https://github.com/NVIDIAGameWorks/kaolin/blob/master/LICENSE.NSCL).
+
+## Usage
+
+```
+python example_cutting.py /path/to/mesh.obj [-qm quadrature_model.pt]
+```
+
+Run without options to see full list of possible arguments
+
+Interactive commands:
+
+ - Ctrl+left mouse: add material
+ - Ctrl+right mouse: remove material
+ - Shift+left drag: apply picking force
+
+
+
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/scripts/example_cutting/__init__.py b/deps/vomp/vomp/fem/scripts/example_cutting/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/fem/scripts/example_cutting/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/fem/scripts/example_cutting/embedded_sim_utils.py b/deps/vomp/vomp/fem/scripts/example_cutting/embedded_sim_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..7782cc3184d80dc3dd4dc772b591fb14c16bf13d
--- /dev/null
+++ b/deps/vomp/vomp/fem/scripts/example_cutting/embedded_sim_utils.py
@@ -0,0 +1,475 @@
+# Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved.
+# NVIDIA CORPORATION and its licensors retain all intellectual property
+# and proprietary rights in and to this software, related documentation
+# and any modifications thereto. Any use, reproduction, disclosure or
+# distribution of this software and related documentation without an express
+# license agreement from NVIDIA CORPORATION is strictly prohibited.
+
+"""Utilities for setting-up embedded simulation of surfaces in hex meshes"""
+
+import functools
+import numpy as np
+import torch
+from kaolin.non_commercial import FlexiCubes
+
+import warp as wp
+import warp.fem as fem
+
+FC_WEIGHT_SCALE = 0.95
+"""alpha-parameter scaling for Flexicubes"""
+
+
+def infer_quadrature(model, cube, sdf, weight):
+ """Inferred quadrature points from MLP"""
+
+ cell_sdf = sdf[cube]
+ cell_alpha = 1.0 + FC_WEIGHT_SCALE * torch.tanh(weight[:, 12:20])
+
+ qc, qw = model(cell_sdf * cell_alpha)
+
+ qc = qc.float().flip(dims=(2,)) # flip because FC cube corners are z-major
+
+ min_sdf, _ = torch.min(cell_sdf, dim=1)
+ active_cells = torch.where(min_sdf < 0, 1, 0)
+
+ qc = qc.contiguous()
+ qw = (qw + 1.0e-8).contiguous()
+
+ return qc, qw, active_cells
+
+
+def regular_quadrature(cube, sdf, weight, clip=True, order=2):
+ """Regular Gauss_Legendre quadrature points, possibly clipped"""
+
+ cell_sdf = sdf[cube]
+ cell_alpha = 1.0 + FC_WEIGHT_SCALE * torch.tanh(weight[:, 12:20])
+
+ reg_points, reg_weights = fem.geometry.element.Cube().instantiate_quadrature(
+ order=order, family=fem.Polynomial.GAUSS_LEGENDRE
+ )
+
+ reg_qp = torch.tensor(reg_points, device="cuda", dtype=torch.float32)
+ reg_qw = torch.tensor(reg_weights, device="cuda", dtype=torch.float32)
+
+ n_qp = len(reg_qw)
+
+ min_sdf, _ = torch.min(cell_sdf, dim=1)
+ active_cells = torch.where(min_sdf < 0, 1, 0)
+
+ qc = torch.zeros(
+ size=(cell_sdf.shape[0], n_qp, 3), dtype=torch.float32, device="cuda"
+ )
+ qw = torch.zeros(size=(cell_sdf.shape[0], n_qp), dtype=torch.float32, device="cuda")
+
+ qc[:] = reg_qp
+ qw[:] = reg_qw
+
+ if clip:
+ x = qc[:, :, 0]
+ y = qc[:, :, 1]
+ z = qc[:, :, 2]
+
+ cell_s = (cell_sdf * cell_alpha).unsqueeze(-1)
+ s = (
+ (1.0 - x) * (1.0 - y) * (1.0 - z) * cell_s[:, 0]
+ + (x) * (1.0 - y) * (1.0 - z) * cell_s[:, 1]
+ + (1.0 - x) * (y) * (1.0 - z) * cell_s[:, 2]
+ + (x) * (y) * (1.0 - z) * cell_s[:, 3]
+ + (1.0 - x) * (1.0 - y) * (z) * cell_s[:, 4]
+ + (x) * (1.0 - y) * (z) * cell_s[:, 5]
+ + (1.0 - x) * (y) * (z) * cell_s[:, 6]
+ + (x) * (y) * (z) * cell_s[:, 7]
+ )
+
+ qw *= torch.where(s <= 0.0, 1.0, 0.0)
+
+ qc = qc.contiguous()
+ qw = (qw + 1.0e-8).contiguous()
+
+ return qc, qw, active_cells
+
+
+@functools.cache
+def _load_model(model_path: str):
+ model = torch.jit.load(model_path)
+ model.eval()
+ return model
+
+
+def get_quadrature(model_path, cube, sdf, weight, clip=True, order=0):
+ sdf = torch.tensor(sdf, device="cuda")
+ cube = torch.tensor(cube, device="cuda")
+ weight = torch.tensor(weight, device="cuda")
+
+ if model_path is None:
+ qc, qw, active_cells = regular_quadrature(
+ cube, sdf, weight, clip=clip, order=order
+ )
+ else:
+ model = _load_model(model_path)
+ qc, qw, active_cells = infer_quadrature(model, cube, sdf, weight)
+
+ qc_wp = wp.clone(wp.from_torch(qc, dtype=wp.vec3, requires_grad=False))
+ qw_wp = wp.clone(wp.from_torch(qw, dtype=wp.float32, requires_grad=False))
+ active_cells = wp.clone(
+ wp.from_torch(active_cells.int(), dtype=wp.int32, requires_grad=False)
+ )
+
+ return qc_wp, qw_wp, active_cells
+
+
+@wp.kernel
+def surface_vertex_cell_index(
+ cube_vbeg: wp.array(dtype=int),
+ cube_nv: wp.array(dtype=int),
+ sorted_vidx: wp.array(dtype=int),
+ tri_vtx_cell_index: wp.array(dtype=int),
+):
+ # Map tri vertices to embedding cell index
+
+ c = wp.tid()
+ end = cube_vbeg[c]
+ beg = end - cube_nv[c]
+
+ for v in range(beg, end):
+ sorted_v = sorted_vidx[v]
+ tri_vtx_cell_index[sorted_v] = c
+
+
+@fem.integrand
+def surface_vertex_coords(
+ s: fem.Sample,
+ domain: fem.Domain,
+ vertex_pos: wp.array(dtype=wp.vec3),
+ vertex_coords: wp.array(dtype=fem.Coords),
+):
+ v = s.qp_index
+ v_pos = vertex_pos[v]
+
+ # dX/dc
+ coords = s.element_coords
+ for _k in range(64):
+ s = fem.types.make_free_sample(s.element_index, coords)
+ pos = domain(s)
+ F = fem.deformation_gradient(domain, s)
+
+ coords += 0.25 * wp.inverse(F) * (v_pos - pos)
+ coords = wp.vec3(
+ wp.clamp(coords[0], 0.0, 1.0),
+ wp.clamp(coords[1], 0.0, 1.0),
+ wp.clamp(coords[2], 0.0, 1.0),
+ )
+
+ err = wp.length(pos - v_pos)
+ if wp.length(pos - v_pos) > 0.1 * wp.cbrt(fem.measure(domain, s)):
+ wp.printf("Failed to embed vertex %d, error= %f \n", v, err)
+
+ vertex_coords[v] = coords
+
+
+@fem.integrand
+def surface_positions(s: fem.Sample, domain: fem.Domain, displacement: fem.Field):
+ return domain(s) + displacement(s)
+
+
+@fem.integrand
+def element_bbox(
+ s: fem.Sample,
+ domain: fem.Domain,
+ bbox_min: wp.array(dtype=wp.vec3),
+ bbox_max: wp.array(dtype=wp.vec3),
+):
+ x = domain(s)
+ wp.atomic_min(bbox_min, s.element_index, x)
+ wp.atomic_max(bbox_max, s.element_index, x)
+
+
+def embed_points(domain: fem.GeometryDomain, points: wp.array(dtype=wp.vec3)):
+ cell_count = domain.element_count()
+ device = points.device
+
+ lowers = wp.empty(cell_count, dtype=wp.vec3)
+ uppers = wp.empty(cell_count, dtype=wp.vec3)
+
+ bb_quadrature = fem.RegularQuadrature(
+ fem.Cells(domain.geometry), order=2, family=fem.Polynomial.EQUISPACED_CLOSED
+ )
+
+ @wp.kernel
+ def _compute_bbox(
+ domain_index_arg: domain.ElementIndexArg,
+ domain_arg: domain.ElementArg,
+ qp_arg: bb_quadrature.Arg,
+ bbox_min: wp.array(dtype=wp.vec3),
+ bbox_max: wp.array(dtype=wp.vec3),
+ ):
+ i = wp.tid()
+ element_index = domain.element_index(domain_index_arg, i)
+
+ qp_count = bb_quadrature.point_count(domain_arg, qp_arg, i, element_index)
+
+ lower = wp.vec3(1.0e16)
+ upper = wp.vec3(-1.0e16)
+
+ for k in range(qp_count):
+ coords = bb_quadrature.point_coords(domain_arg, qp_arg, i, element_index, k)
+ x = domain.element_position(
+ domain_arg, fem.make_free_sample(element_index, coords)
+ )
+ lower = wp.min(lower, x)
+ upper = wp.max(upper, x)
+
+ bbox_min[i] = lower
+ bbox_max[i] = upper
+
+ with wp.ScopedTimer("AABB", synchronize=True):
+ wp.launch(
+ _compute_bbox,
+ cell_count,
+ inputs=[
+ domain.element_index_arg_value(device),
+ domain.element_arg_value(device),
+ bb_quadrature.arg_value(device),
+ ],
+ outputs=[lowers, uppers],
+ )
+
+ with wp.ScopedTimer("BVH", synchronize=True):
+ bvh = wp.Bvh(lowers=lowers, uppers=uppers)
+
+ @wp.kernel
+ def _lookup(
+ domain_index_arg: domain.ElementIndexArg,
+ domain_arg: domain.ElementArg,
+ bvh: wp.uint64,
+ points: wp.array(dtype=wp.vec3),
+ cell_indices: wp.array(dtype=int),
+ cell_coords: wp.array(dtype=fem.Coords),
+ ):
+ i = wp.tid()
+ p = points[i]
+
+ query = wp.bvh_query_aabb(bvh, p, p)
+ domain_cell_idx = int(-1)
+
+ min_cell = int(-1)
+ min_coords = fem.Coords()
+ min_dist = float(1.0e8)
+
+ eps = 0.0001
+
+ while wp.bvh_query_next(query, domain_cell_idx) and min_dist > eps:
+ cell_idx = domain.element_index(domain_index_arg, domain_cell_idx)
+ s = fem.make_free_sample(cell_idx, fem.Coords(0.5))
+
+ for _k in range(16):
+ x = domain.element_position(domain_arg, s)
+ dx = p - x
+ dist = wp.length(dx)
+ if dist < min_dist:
+ min_dist = dist
+ min_coords = s.element_coords
+ min_cell = cell_idx
+
+ if dist < eps:
+ break
+
+ F = domain.element_deformation_gradient(domain_arg, s)
+
+ dc = wp.inverse(F) * dx
+ coords = s.element_coords + 0.5 * dc
+
+ s.element_coords = wp.vec3(
+ wp.clamp(coords[0], 0.0, 1.0),
+ wp.clamp(coords[1], 0.0, 1.0),
+ wp.clamp(coords[2], 0.0, 1.0),
+ )
+
+ cell_indices[i] = min_cell
+ cell_coords[i] = min_coords
+
+ cell_indices = wp.empty(points.shape, dtype=int)
+ cell_coords = wp.empty(points.shape, dtype=fem.Coords)
+
+ with wp.ScopedTimer("Lookup", synchronize=True):
+ wp.launch(
+ _lookup,
+ points.shape,
+ inputs=[
+ domain.element_index_arg_value(device),
+ domain.element_arg_value(device),
+ bvh.id,
+ points,
+ ],
+ outputs=[cell_indices, cell_coords],
+ )
+
+ return fem.PicQuadrature(domain, positions=(cell_indices, cell_coords))
+
+
+def embed_tri_mesh(
+ domain: fem.GeometryDomain,
+ tri_vtx_pos,
+ bd_cubes,
+ bd_nv,
+):
+ """Embeds mesh vertices from a Flexicubes surface in a hexmesh
+
+ TODO: switch to using BVH queries instead of explicit indexing
+
+ Args:
+ tri_vtx_pos: points to embed
+ bd_cubes: indices of hexes containing points
+ bd_nv: number of points per hex
+ """
+
+ v_cell_idx = np.concatenate(
+ (
+ np.nonzero(bd_nv == 1),
+ np.repeat(np.nonzero(bd_nv == 2), 2),
+ np.repeat(np.nonzero(bd_nv == 3), 3),
+ np.repeat(np.nonzero(bd_nv == 4), 4),
+ ),
+ axis=None,
+ )
+
+ sorted_vidx = np.argsort(v_cell_idx)
+ sorted_vidx = wp.array(sorted_vidx, dtype=int)
+
+ cube_nv = np.zeros(domain.geometry_element_count(), dtype=int)
+ cube_vbeg = np.zeros(domain.geometry_element_count(), dtype=int)
+
+ cube_nv[bd_cubes] = bd_nv
+ cube_vbeg = np.cumsum(cube_nv)
+
+ cube_vbeg = wp.array(cube_vbeg, dtype=int)
+ cube_nv = wp.array(cube_nv, dtype=int)
+ tri_vtx_cell_index = wp.empty(shape=tri_vtx_pos.shape, dtype=int)
+
+ wp.launch(
+ surface_vertex_cell_index,
+ dim=cube_nv.shape[0],
+ inputs=[cube_vbeg, cube_nv, sorted_vidx, tri_vtx_cell_index],
+ )
+ tri_vtx_coords = wp.zeros_like(tri_vtx_pos)
+ vtx_quadrature = fem.PicQuadrature(
+ domain=domain, positions=(tri_vtx_cell_index, tri_vtx_coords)
+ )
+
+ fem.interpolate(
+ surface_vertex_coords,
+ quadrature=vtx_quadrature,
+ values={"vertex_pos": tri_vtx_pos, "vertex_coords": tri_vtx_coords},
+ )
+
+ return vtx_quadrature
+
+
+def sim_from_flexicubes(sim_class, flexi, geo: fem.Grid3D, args):
+ """Instantiates a simulator instance from Flexicubes data"""
+
+ fc_sdf = flexi["fc_sdf"]
+ fc_pos = flexi["fc_pos"]
+ fc_weights = flexi["fc_weights"]
+ fc_stiff = flexi["fc_stiffness"]
+ fc_cube = flexi["fc_cube"]
+
+ res = geo.res[0]
+
+ # Compute quadrature and active cells from flexicube sdf
+ quad_model = None if args.reg_qp else args.quadrature_model
+ quad_order = max(2 * args.degree, args.reg_qp)
+ qc, qw, active_cells = get_quadrature(
+ quad_model, fc_cube, fc_sdf, fc_weights, clip=args.clip, order=quad_order
+ )
+
+ # Create deformed grid
+ grid_displacement_field = fem.make_polynomial_space(
+ geo, degree=1, dtype=wp.vec3
+ ).make_field()
+ grid_displacement_field.dof_values = fc_pos
+ grid_displacement_field.dof_values.requires_grad = True
+
+ deformed_grid = grid_displacement_field.make_deformed_geometry(relative=False)
+
+ # Initialize sim
+ sim = sim_class(deformed_grid, active_cells, args)
+
+ sim.forces.count = 0
+ sim.forces.centers = wp.zeros(
+ shape=(1,),
+ dtype=wp.vec3,
+ )
+ sim.forces.radii = wp.array([2.0 / res], dtype=float)
+ sim.forces.forces = wp.array([[0.0, 0.0, 0.0]], dtype=wp.vec3)
+
+ sim.init_displacement_space()
+ if fc_stiff is not None:
+ sim.scale_lame_field(wp.array(fc_stiff, dtype=float))
+
+ # Replace regular quadrature will learned quadrature
+ domain = fem.Cells(sim._geo_partition)
+ quadrature = fem.ExplicitQuadrature(domain, qc, qw)
+ sim.vel_quadrature = quadrature
+ sim.strain_quadrature = quadrature
+ sim.elasticity_quadrature = quadrature
+
+ # For Mixed FEM: locate strain nodes at quadrature points
+ geo_quadrature = fem.ExplicitQuadrature(fem.Cells(deformed_grid), qc, qw)
+ rbf_basis = fem.PointBasisSpace(geo_quadrature)
+ sim.set_strain_basis(rbf_basis)
+ sim.init_strain_spaces()
+
+ return sim
+
+
+def flexicubes_from_sdf_grid(
+ res, sdf, pos, sdf_grad_func=None, output_tetmesh=False, device="cuda"
+):
+ """Creates a Flexicubes datastructure from a SDF discretized on a dense grid"""
+
+ fc = FlexiCubes(device)
+ x_nx3, cube_fx8 = fc.construct_voxel_grid(res)
+
+ weight = torch.zeros((cube_fx8.shape[0], 21), dtype=torch.float, device=device)
+
+ flexi = {
+ "fc_pos": pos.detach().cpu().numpy(),
+ "fc_sdf": sdf.detach().cpu().numpy(),
+ "fc_cube": cube_fx8.detach().cpu().numpy(),
+ "fc_weights": weight.detach().cpu().numpy(),
+ "fc_stiffness": None,
+ }
+
+ vertices, faces, L_dev = fc(
+ voxelgrid_vertices=pos,
+ scalar_field=sdf,
+ cube_idx=cube_fx8,
+ resolution=res,
+ weight_scale=FC_WEIGHT_SCALE,
+ beta=weight[:, :12],
+ alpha=weight[:, 12:20],
+ gamma_f=weight[:, 20],
+ training=False,
+ output_tetmesh=output_tetmesh,
+ grad_func=sdf_grad_func,
+ )
+
+ if output_tetmesh:
+ flexi.update(
+ {
+ "tet_vertices": vertices.detach().cpu().numpy(),
+ "tet_indices": faces.detach().cpu().numpy(),
+ "vtx_displ": np.zeros(vertices.shape, dtype=np.float32),
+ }
+ )
+ else:
+ flexi.update(
+ {
+ "tri_vertices": vertices.detach().cpu().numpy(),
+ "tri_faces": faces.detach().cpu().numpy(),
+ "vtx_displ": np.zeros(vertices.shape, dtype=np.float32),
+ }
+ )
+
+ return flexi
diff --git a/deps/vomp/vomp/fem/scripts/example_cutting/example_cutting.py b/deps/vomp/vomp/fem/scripts/example_cutting/example_cutting.py
new file mode 100644
index 0000000000000000000000000000000000000000..01262e7138970c732a0f3d25621be3a27babbdbc
--- /dev/null
+++ b/deps/vomp/vomp/fem/scripts/example_cutting/example_cutting.py
@@ -0,0 +1,601 @@
+# Copyright (c) 2025 NVIDIA CORPORATION. All rights reserved.
+# NVIDIA CORPORATION and its licensors retain all intellectual property
+# and proprietary rights in and to this software, related documentation
+# and any modifications thereto. Any use, reproduction, disclosure or
+# distribution of this software and related documentation without an express
+# license agreement from NVIDIA CORPORATION is strictly prohibited.
+
+import argparse
+
+import numpy as np
+import polyscope as ps
+import polyscope.imgui as psim
+import torch
+from vomp.fem.scripts.example_cutting.embedded_sim_utils import (
+ flexicubes_from_sdf_grid,
+ sim_from_flexicubes,
+ surface_positions,
+)
+from kaolin.io import import_mesh
+
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM
+from vomp.fem.fem_examples.mfem.collisions import CollisionHandler
+
+import warp as wp
+import warp.fem as fem
+from warp.sim.collide import triangle_closest_point, TRI_CONTACT_FEATURE_FACE_INTERIOR
+
+torch.cuda.set_per_process_memory_fraction(0.5)
+
+
+def load_mesh(path):
+ """Load and normalize an obj mesh from path"""
+
+ mesh = import_mesh(path, triangulate=True).cuda()
+ # normalize to [-1, 1]
+ half_bbox = (
+ 0.5 * torch.min(mesh.vertices, dim=0)[0],
+ 0.5 * torch.max(mesh.vertices, dim=0)[0],
+ )
+ normalized_vertices = (mesh.vertices - half_bbox[0] - half_bbox[1]) / torch.max(
+ half_bbox[1] - half_bbox[0] + 0.001
+ )
+ return wp.Mesh(
+ wp.from_torch(normalized_vertices, dtype=wp.vec3),
+ wp.from_torch(mesh.faces.flatten().to(torch.int32)),
+ support_winding_number=True,
+ )
+
+
+@fem.integrand
+def fixed_points_projector_form(
+ s: fem.Sample,
+ domain: fem.Domain,
+ u_cur: fem.Field,
+ u: fem.Field,
+ v: fem.Field,
+):
+ """Dirichlet boundary condition projector
+
+ Here we simply clamp points near Z boundaries
+ """
+
+ y = domain(s)
+
+ clamped = wp.where(wp.abs(y[1]) > 0.9, 1.0, 0.0)
+
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@wp.kernel
+def world_to_rest_pose_kernel(
+ mesh: wp.uint64,
+ rest_points: wp.array(dtype=wp.vec3),
+ pos: wp.vec3,
+ out: wp.array(dtype=wp.vec3),
+):
+ """
+ Converts a point on the deformed surface to its rest-pose counterpart
+ """
+
+ max_dist = 1.0
+ query = wp.mesh_query_point_no_sign(mesh, pos, max_dist)
+
+ if query.result:
+ faces = wp.mesh_get(mesh).indices
+ v0 = rest_points[faces[3 * query.face + 0]]
+ v1 = rest_points[faces[3 * query.face + 1]]
+ v2 = rest_points[faces[3 * query.face + 2]]
+
+ p = v0 + query.u * (v1 - v0) + query.v * (v2 - v0)
+
+ else:
+ p = pos
+
+ out[0] = p
+
+
+@wp.kernel
+def mesh_sdf_kernel(
+ mesh: wp.uint64,
+ points: wp.array(dtype=wp.vec3),
+ sdf: wp.array(dtype=float),
+):
+ """Builds an SDF using mesh closest-point queries"""
+
+ i = wp.tid()
+ pos = points[i]
+
+ max_dist = 1.0
+ query = wp.mesh_query_point_sign_winding_number(mesh, pos, max_dist)
+
+ if query.result:
+ mesh_pos = wp.mesh_eval_position(mesh, query.face, query.u, query.v)
+ sdf[i] = query.sign * wp.length(pos - mesh_pos)
+ else:
+ sdf[i] = 1.0
+
+
+@wp.kernel
+def detect_self_collisions(
+ cur_contacts: int,
+ max_contacts: int,
+ dt: float,
+ self_immunity_ratio: float,
+ mesh_id: wp.uint64,
+ mesh_rest_pos: wp.array(dtype=wp.vec3),
+ du_cur: wp.array(dtype=wp.vec3),
+ radius: float,
+ count: wp.array(dtype=int),
+ normals: wp.array(dtype=wp.vec3),
+ kinematic_gaps: wp.array(dtype=wp.vec3),
+ indices_a: wp.array(dtype=int),
+ indices_b: wp.array(dtype=int),
+ pos_b: wp.array(dtype=wp.vec3),
+):
+ tid = wp.tid()
+ mesh = wp.mesh_get(mesh_id)
+
+ x = mesh.points[tid]
+
+ lower = x - wp.vec3(radius)
+ upper = x + wp.vec3(radius)
+
+ query = wp.mesh_query_aabb(mesh_id, lower, upper)
+
+ face_index = wp.int32(0)
+ while wp.mesh_query_aabb_next(query, face_index):
+ t0 = mesh.indices[3 * face_index + 0]
+ t1 = mesh.indices[3 * face_index + 1]
+ t2 = mesh.indices[3 * face_index + 2]
+ if tid == t0 or tid == t1 or tid == t2:
+ # Fast self collision
+ continue
+
+ u1 = mesh.points[t0]
+ u2 = mesh.points[t1]
+ u3 = mesh.points[t2]
+
+ cp, bary, feature_type = triangle_closest_point(u1, u2, u3, x)
+ if feature_type != TRI_CONTACT_FEATURE_FACE_INTERIOR:
+ continue
+
+ delta = x - cp
+
+ face_nor = wp.mesh_eval_face_normal(mesh_id, face_index)
+ sign = wp.where(wp.dot(delta, face_nor) > 0.0, 1.0, -1.0)
+
+ dist = wp.length(delta) * sign
+
+ if dist < radius:
+ # discard self-collisions of points that were very close at rest
+ rp0 = mesh_rest_pos[t0]
+ rp1 = mesh_rest_pos[t1]
+ rp2 = mesh_rest_pos[t2]
+ xb_rest = bary[0] * rp0 + bary[1] * rp1 + bary[2] * rp2
+ xa_rest = mesh_rest_pos[tid]
+ if wp.length(xb_rest - xa_rest) < self_immunity_ratio * radius:
+ continue
+
+ idx = wp.atomic_add(count, 0, 1)
+ if idx >= max_contacts:
+ return
+
+ if dist < 0.00001:
+ n = face_nor
+ else:
+ n = wp.normalize(delta) * sign
+ normals[idx] = n
+
+ du0 = du_cur[t0]
+ du1 = du_cur[t1]
+ du2 = du_cur[t2]
+ du = du_cur[tid] - du0 * bary[0] - du1 * bary[1] - du2 * bary[2]
+
+ kinematic_gap = (dist - wp.dot(du, n)) * n
+ kinematic_gaps[idx] = kinematic_gap
+ indices_a[idx] = tid
+ indices_b[idx] = mesh.points.shape[0] + idx - cur_contacts
+ pos_b[idx - cur_contacts] = xb_rest
+
+
+class SelfCollisionHandler(CollisionHandler):
+ def __init__(
+ self,
+ vtx_quadrature: fem.PicQuadrature,
+ tri_mesh: wp.Mesh,
+ sim: ClassicFEM,
+ ):
+ super().__init__(
+ [], vtx_quadrature.cell_indices, vtx_quadrature.particle_coords, sim
+ )
+
+ self.tri_vtx_quadrature = vtx_quadrature
+ self.vtx_rest_pos = wp.clone(tri_mesh.points)
+ self.tri_mesh = tri_mesh
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ CollisionHandler.add_parser_arguments(parser)
+ parser.add_argument(
+ "--self_immunity_radius_ratio",
+ "-cs",
+ type=float,
+ default=4.0,
+ help="Ignore self-collision for particles that were within this ratio at rest",
+ )
+
+ def run_collision_detectors(
+ self,
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ ):
+ self.set_collision_quadrature(self.tri_vtx_quadrature)
+
+ super().run_collision_detectors(
+ dt,
+ count,
+ indices_a,
+ indices_b,
+ normals,
+ kinematic_gaps,
+ )
+
+ self.cp_world_position(dest=self.tri_mesh.points)
+ self.tri_mesh.refit()
+
+ cp_du = self._sample_cp_displacement(self.sim.du_field)
+
+ n_cp = cp_du.shape[0]
+ max_contacts = self.collision_normals.shape[0]
+
+ collision_radius = (
+ self.args.collision_radius * self.args.collision_detection_ratio
+ )
+
+ start_contacts = count.numpy()[0]
+ pos_b = wp.empty(indices_b.shape, dtype=wp.vec3)
+
+ wp.launch(
+ detect_self_collisions,
+ dim=(n_cp),
+ inputs=[
+ start_contacts,
+ max_contacts,
+ dt,
+ self.args.self_immunity_radius_ratio,
+ self.tri_mesh.id,
+ self.vtx_rest_pos,
+ cp_du,
+ collision_radius,
+ count,
+ normals,
+ kinematic_gaps,
+ indices_a,
+ indices_b,
+ pos_b,
+ ],
+ )
+ self_contacts = int(min(max_contacts, count.numpy()[0]) - start_contacts)
+
+ if self_contacts > 0:
+ contact_points = wp.empty(n_cp + self_contacts, dtype=wp.vec3)
+ wp.copy(contact_points[:n_cp], self.vtx_rest_pos)
+ wp.copy(contact_points[n_cp:], pos_b[:self_contacts])
+
+ quadrature = fem.PicQuadrature(fem.Cells(self.sim.geo.base), contact_points)
+ quadrature.domain = self.collision_quadrature.domain
+ self.set_collision_quadrature(quadrature)
+
+
+class SelfCollidingSim(ClassicFEM):
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ ClassicFEM.add_parser_arguments(parser)
+ SelfCollisionHandler.add_parser_arguments(parser)
+
+ def compute_initial_guess(self):
+ self.du_field.dof_values.zero_()
+ self.collision_handler.detect_collisions(self.dt)
+
+ def evaluate_energy(self):
+ E_p, c_r = super().evaluate_energy()
+
+ E_p = self.collision_handler.add_collision_energy(E_p)
+
+ return E_p, c_r
+
+ def newton_lhs(self):
+ lhs = super().newton_lhs()
+ self.collision_handler.add_collision_hessian(lhs)
+ fem.dirichlet.project_system_matrix(lhs, self.v_bd_matrix)
+
+ return lhs
+
+ def newton_rhs(self, tape=None):
+ rhs = super().newton_rhs(tape)
+ self.collision_handler.add_collision_forces(rhs)
+ self._filter_forces(rhs, tape=tape)
+ return rhs
+
+ def prepare_newton_step(self, tape=None):
+ self.collision_handler.prepare_newton_step(self.dt)
+
+ return super().prepare_newton_step(tape)
+
+ def init_collision_detector(
+ self,
+ vtx_quadrature: fem.PicQuadrature,
+ tri_mesh: wp.Mesh,
+ ):
+ self.collision_handler = SelfCollisionHandler(vtx_quadrature, tri_mesh, self)
+
+
+class Clay:
+ """Utility struct for storing simulation state"""
+
+ def __init__(self, geo):
+ self.geo = geo
+
+ self.sim = None
+ self.tri_mesh = None
+ self.tri_vtx_quadrature = None
+ self.rest_points = None
+
+ def create_sim(self, flexicubes):
+ if self.sim is not None:
+ # save previous displacement and velocity
+ prev_displacement_field = self.sim.u_field.space.make_field()
+ prev_velocity_field = self.sim.du_field.space.make_field()
+ fem.interpolate(self.sim.u_field, dest=prev_displacement_field)
+ fem.interpolate(self.sim.du_field, dest=prev_velocity_field)
+ prev_displacement = prev_displacement_field.dof_values
+ prev_velocity = prev_velocity_field.dof_values
+ else:
+ prev_displacement = None
+ prev_velocity = None
+
+ # (Re)create simulation
+ self.sim = sim_from_flexicubes(SelfCollidingSim, flexicubes, geo, args)
+ self.sim.set_fixed_points_condition(
+ fixed_points_projector_form,
+ )
+ self.sim.init_constant_forms()
+ self.sim.project_constant_forms()
+
+ # Interpolate back previous displacement
+ if prev_displacement is not None:
+ prev_displacement_field = self.sim.u_field.space.make_field()
+ prev_displacement_field.dof_values = prev_displacement
+ prev_velocity_field = self.sim.du_field.space.make_field()
+ prev_velocity_field.dof_values = prev_velocity
+ fem.interpolate(prev_displacement_field, dest=self.sim.u_field)
+ fem.interpolate(prev_velocity_field, dest=self.sim.du_field)
+
+ # Embed triangle mesh
+ tri_vertices = flexicubes["tri_vertices"]
+ tri_faces = wp.array(flexicubes["tri_faces"], dtype=int).flatten()
+ tri_vtx_pos = wp.array(tri_vertices, dtype=wp.vec3)
+
+ self.tri_mesh = wp.Mesh(tri_vtx_pos, tri_faces)
+ self.rest_points = wp.clone(tri_vtx_pos)
+ self.surface_vtx_quadrature = fem.PicQuadrature(fem.Cells(geo), tri_vtx_pos)
+ self.surface_vtx_quadrature.domain = self.sim.u_test.domain
+
+ self.sim.init_collision_detector(self.surface_vtx_quadrature, self.tri_mesh)
+
+ def world_to_rest_pos(self, world_pos):
+ tri_mesh = self.tri_mesh
+
+ rest_pos = wp.empty(1, dtype=wp.vec3)
+ tri_mesh.refit()
+ wp.launch(
+ world_to_rest_pose_kernel,
+ dim=1,
+ inputs=[tri_mesh.id, self.rest_points, world_pos, rest_pos],
+ )
+ return rest_pos
+
+
+if __name__ == "__main__":
+ wp.init()
+ wp.set_module_options({"enable_backward": False})
+ wp.set_module_options({"fast_math": True})
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("mesh")
+ parser.add_argument("--quadrature_model", "-qm", default=None)
+ parser.add_argument("--force_scale", type=float, default=1.0)
+ parser.add_argument("--resolution", type=int, default=64)
+
+ SelfCollidingSim.add_parser_arguments(parser)
+ args = parser.parse_args()
+
+ # fall back to full-cell quadrature if neural model not provided
+ args.reg_qp = 2 if args.quadrature_model is None else 0
+ args.clip = False
+
+ args.ground_height = -1.0
+ args.collision_radius = 0.5 / args.resolution
+
+ res = args.resolution
+ geo = fem.Grid3D(res=wp.vec3i(res), bounds_lo=wp.vec3(-1), bounds_hi=wp.vec3(1))
+
+ # sample mesh SDF on grid nodes
+ source_mesh = load_mesh(args.mesh)
+ grid_node_pos = fem.make_polynomial_space(geo).node_positions()
+ grid_sdf = wp.empty(grid_node_pos.shape[0], dtype=float)
+ wp.launch(
+ mesh_sdf_kernel,
+ dim=grid_node_pos.shape,
+ inputs=[source_mesh.id, grid_node_pos, grid_sdf],
+ )
+ grid_sdf = wp.to_torch(grid_sdf)
+ grid_node_pos = wp.to_torch(grid_node_pos)
+
+ # Create
+ flexicubes = flexicubes_from_sdf_grid(
+ res, pos=grid_node_pos, sdf=grid_sdf, sdf_grad_func=None
+ )
+
+ # Create simulation
+ clay = Clay(geo)
+ clay.create_sim(flexicubes)
+
+ # Add hooks for displaying surface and run sim
+
+ def init_surface(flexicubes):
+ tri_vertices = flexicubes["tri_vertices"]
+ tri_faces = flexicubes["tri_faces"]
+
+ surface = ps.register_surface_mesh("surf", tri_vertices, tri_faces)
+ surface.set_edge_width(1.0)
+
+ prev_world_pos = None
+
+ # user interface callback
+ def callback():
+ global grid_sdf, prev_world_pos, force_center_quadrature
+
+ io = psim.GetIO()
+
+ if io.KeyMods in (1, psim.ImGuiModFlags_Ctrl):
+ # ctrl + mouse: update SDF values
+
+ if io.MouseDown[0]:
+ sign = -1.0 # left-mouse, add material
+ elif io.MouseDown[1]:
+ sign = 1.0 # right-mouse, remove material
+ else:
+ sign = 0.0
+
+ if sign != 0.0:
+ screen_coords = io.MousePos
+
+ # Convert clicked position to rest pose
+ world_pos = ps.screen_coords_to_world_position(screen_coords)
+ if np.all(np.isfinite(world_pos)):
+ prev_world_pos = world_pos
+ elif prev_world_pos is not None:
+ world_pos = prev_world_pos
+
+ rest_pos = wp.to_torch(clay.world_to_rest_pos(world_pos))
+
+ # locally update sdf values
+ delta_pos_sq = torch.sum(
+ (grid_node_pos - rest_pos) * (grid_node_pos - rest_pos), dim=1
+ )
+ delta_sdf = torch.exp(-0.25 * delta_pos_sq * res * res)
+
+ grid_sdf += 50.0 * sign / res * delta_sdf
+
+ # rebuilds flexicubes structure and recreate sim
+ flexicubes = flexicubes_from_sdf_grid(res, grid_sdf, grid_node_pos)
+ clay.create_sim(flexicubes)
+ init_surface(flexicubes)
+
+ io.WantCaptureMouse = True
+
+ sim = clay.sim
+
+ # run one frame of simulation
+ sim.run_frame()
+
+ # Interpolate deformation back to vertices
+ fem.interpolate(
+ surface_positions,
+ quadrature=clay.surface_vtx_quadrature,
+ dest=clay.tri_mesh.points,
+ fields={"displacement": sim.u_field},
+ )
+ surf_mesh = ps.get_surface_mesh("surf")
+ surf_mesh.update_vertex_positions(clay.tri_mesh.points.numpy())
+
+ ps.register_point_cloud(
+ "CP",
+ sim.collision_handler.cp_world_position().numpy()[
+ clay.tri_mesh.points.shape[0] :
+ ],
+ )
+
+ # Dynamic picking force
+ # (shift + click)
+
+ if io.KeyMods in (2, psim.ImGuiModFlags_Shift): # shift
+ if io.MouseClicked[0]:
+ screen_coords = io.MousePos
+ world_ray = ps.screen_coords_to_world_ray(screen_coords)
+ world_pos = ps.screen_coords_to_world_position(screen_coords)
+
+ if np.all(np.isfinite(world_pos)):
+ rest_pos = clay.world_to_rest_pos(world_pos)
+
+ # update force application point
+ sim.forces.count = 1
+ sim.forces.forces.zero_()
+ sim.forces.centers = rest_pos
+ sim.update_force_weight()
+
+ # embed force center so we can move it with the sim
+ force_center_quadrature = fem.PicQuadrature(
+ fem.Cells(geo),
+ rest_pos,
+ )
+ force_center_quadrature._domain = sim.u_test.domain
+
+ else:
+ sim.forces.count = 0
+
+ elif sim.forces.count > 0:
+ screen_coords = io.MousePos
+ world_ray = ps.screen_coords_to_world_ray(screen_coords)
+
+ # interpolate current position of force application center
+ force_center_position = wp.empty(shape=(1,), dtype=wp.vec3)
+ fem.interpolate(
+ surface_positions,
+ quadrature=force_center_quadrature,
+ dest=force_center_position,
+ fields={"displacement": sim.u_field},
+ )
+ deformed_force_center = force_center_position.numpy()[0]
+
+ # update picking force direction
+ ray_dir = world_ray / np.linalg.norm(world_ray)
+ ray_orig = ps.get_view_camera_parameters().get_position()
+ perp = ray_orig - deformed_force_center
+ perp -= np.dot(perp, ray_dir) * ray_dir
+
+ sim.forces.forces = wp.array([perp * args.force_scale], dtype=wp.vec3)
+
+ # force line visualization
+ ps.get_curve_network("force_line").update_node_positions(
+ np.array([deformed_force_center, deformed_force_center + perp])
+ )
+ ps.get_curve_network("force_line").set_enabled(True)
+
+ if io.MouseReleased[0]:
+ sim.forces.count = 0
+ ps.get_curve_network("force_line").set_enabled(False)
+
+ io.WantCaptureMouse = sim.forces.count > 0
+
+ ps.init()
+
+ ps.set_ground_plane_mode(mode_str="none")
+ ps.register_curve_network(
+ "force_line",
+ nodes=np.zeros((2, 3)),
+ edges=np.array([[0, 1]]),
+ enabled=False,
+ )
+
+ init_surface(flexicubes)
+
+ # ps.set_build_default_gui_panels(False)
+ ps.set_user_callback(callback)
+ ps.show()
diff --git a/deps/vomp/vomp/fem/simulations/cam/compress_two_cubes.json b/deps/vomp/vomp/fem/simulations/cam/compress_two_cubes.json
new file mode 100644
index 0000000000000000000000000000000000000000..e94fa6814277cdb9f0e49a96b215a42a91e78ac5
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/cam/compress_two_cubes.json
@@ -0,0 +1,26 @@
+{
+ "farClipRatio": 20.0,
+ "fov": 45.0,
+ "nearClipRatio": 0.005,
+ "projectionMode": "Perspective",
+ "viewMat": [
+ 0.99999988079071,
+ -5.89352566748857e-10,
+ -1.10594555735588e-07,
+ -1.2378214597702,
+ -6.89178705215454e-08,
+ 0.891952037811279,
+ -0.452130168676376,
+ -0.158852979540825,
+ 5.82076609134674e-08,
+ 0.452130377292633,
+ 0.891952395439148,
+ -2.58306384086609,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ "windowHeight": 516,
+ "windowWidth": 1280
+}
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/cam/grid_cubes_fall_view1.json b/deps/vomp/vomp/fem/simulations/cam/grid_cubes_fall_view1.json
new file mode 100644
index 0000000000000000000000000000000000000000..abf0974aa0c5b0d1ff1c36f353c58600b2fbc8dd
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/cam/grid_cubes_fall_view1.json
@@ -0,0 +1,26 @@
+{
+ "farClipRatio": 20.0,
+ "fov": 45.0,
+ "nearClipRatio": 0.005,
+ "projectionMode": "Perspective",
+ "viewMat": [
+ 0.00204597599804401,
+ -1.19325704872608e-09,
+ -0.999996662139893,
+ 3.48849487304688,
+ -0.392735809087753,
+ 0.919651031494141,
+ -0.000803463743068278,
+ 0.811404228210449,
+ 0.919648885726929,
+ 0.392736673355103,
+ 0.001881442964077,
+ -12.5499753952026,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ "windowHeight": 1080,
+ "windowWidth": 1920
+}
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/cam/grid_cubes_fall_view2.json b/deps/vomp/vomp/fem/simulations/cam/grid_cubes_fall_view2.json
new file mode 100644
index 0000000000000000000000000000000000000000..2eb4587208fef661df9c5f25fef6b7359fa6dfe9
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/cam/grid_cubes_fall_view2.json
@@ -0,0 +1,26 @@
+{
+ "farClipRatio": 20.0,
+ "fov": 45.0,
+ "nearClipRatio": 0.005,
+ "projectionMode": "Perspective",
+ "viewMat": [
+ -0.999921500682831,
+ -1.04046193882823e-09,
+ -0.0124248266220093,
+ 3.53887963294983,
+ -0.00479488214477897,
+ 0.922534883022308,
+ 0.385883897542953,
+ -1.90844631195068,
+ 0.011462340131402,
+ 0.385913670063019,
+ -0.922463238239288,
+ -6.11564636230469,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ "windowHeight": 1080,
+ "windowWidth": 1920
+}
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/cam/interp_cubes_fall.json b/deps/vomp/vomp/fem/simulations/cam/interp_cubes_fall.json
new file mode 100644
index 0000000000000000000000000000000000000000..ea789d107552400a7798d78794f3648aaf904366
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/cam/interp_cubes_fall.json
@@ -0,0 +1,26 @@
+{
+ "farClipRatio": 20.0,
+ "fov": 45.0,
+ "nearClipRatio": 0.005,
+ "projectionMode": "Perspective",
+ "viewMat": [
+ 0.999945402145386,
+ 1.45519152283669e-11,
+ 0.0104166548699141,
+ -3.46171832084656,
+ 0.000808346085250378,
+ 0.996984660625458,
+ -0.0775970593094826,
+ -1.45579504966736,
+ -0.0103851919993758,
+ 0.0776012539863586,
+ 0.996930360794067,
+ -6.07004165649414,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ "windowHeight": 1080,
+ "windowWidth": 1920
+}
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/cam/stretch_two_cubes.json b/deps/vomp/vomp/fem/simulations/cam/stretch_two_cubes.json
new file mode 100644
index 0000000000000000000000000000000000000000..3c70299dddcf3701fe20e55b8c6af41dd9842156
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/cam/stretch_two_cubes.json
@@ -0,0 +1,26 @@
+{
+ "farClipRatio": 20.0,
+ "fov": 45.0,
+ "nearClipRatio": 0.005,
+ "projectionMode": "Perspective",
+ "viewMat": [
+ 1.0,
+ 0.0,
+ 0.0,
+ -1.25,
+ 0.0,
+ 0.997785091400146,
+ -0.0665190070867538,
+ -0.465633064508438,
+ 0.0,
+ 0.0665190070867538,
+ 0.997785091400146,
+ -2.4431746006012,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ "windowHeight": 516,
+ "windowWidth": 1280
+}
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/compress_two_cubes.py b/deps/vomp/vomp/fem/simulations/compress_two_cubes.py
new file mode 100644
index 0000000000000000000000000000000000000000..adbdcae61def596d1c71256ec74aaece64c3e9d6
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/compress_two_cubes.py
@@ -0,0 +1,352 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+import copy
+import json
+import os
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Field, Sample, Domain, integrand, normal
+
+import sys
+import os
+
+# Add the parent directory to the Python path
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM, run_softbody_sim
+
+# -----------------------------------------------------------------------------
+# Geometry helper
+# -----------------------------------------------------------------------------
+
+
+def build_cube_geo(res, offset, height=0.0):
+ lo = wp.vec3(offset[0], height, offset[1])
+ hi = wp.vec3(offset[0] + 1.0, height + 1.0, offset[1] + 1.0)
+ return fem.Grid3D(res=wp.vec3i(res), bounds_lo=lo, bounds_hi=hi)
+
+
+# -----------------------------------------------------------------------------
+# Boundary projectors
+# -----------------------------------------------------------------------------
+
+
+@integrand
+def clamp_bottom_face(s: Sample, domain: Domain, u: Field, v: Field):
+ """Fix bottom nodes (normal.y < 0)."""
+ nor = normal(domain, s)
+ clamped = wp.where(nor[1] < 0.0, 1.0, 0.0)
+ return wp.dot(u(s), v(s)) * clamped
+
+
+# -----------------------------------------------------------------------------
+# Main
+# -----------------------------------------------------------------------------
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser(
+ description="Compress each cube vertically by pushing on its top face"
+ )
+ parser.add_argument("--resolution", type=int, default=10)
+ parser.add_argument(
+ "--spacing", type=float, default=1.5, help="spacing between cube centres"
+ )
+ parser.add_argument(
+ "--youngs",
+ type=float,
+ nargs=2,
+ default=[1e4, 3e4],
+ help="Young's modulus for cube0 cube1",
+ )
+ parser.add_argument(
+ "--poissons",
+ type=float,
+ nargs=2,
+ default=[0.45, 0.45],
+ help="Poisson ratio for cube0 cube1",
+ )
+ parser.add_argument(
+ "--densities",
+ type=float,
+ nargs=2,
+ default=[1000.0, 1000.0],
+ help="Density for cube0 cube1",
+ )
+ parser.add_argument(
+ "--force",
+ type=float,
+ default=1e3,
+ help="Magnitude of downward compressive force (N)",
+ )
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+ parser.add_argument(
+ "--ramp",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Ramp force over frames",
+ )
+
+ # generic FEM args
+ ClassicFEM.add_parser_arguments(parser)
+ parser.set_defaults(
+ n_frames=100,
+ gravity=0.0,
+ quasi_quasistatic=True,
+ young_modulus=1e4,
+ poisson_ratio=0.45,
+ density=1000.0,
+ )
+
+ args = parser.parse_args()
+
+ offsets = [(0.0, 0.0), (args.spacing, 0.0)]
+ sims = []
+ force_sims = [] # list of sims for force ramping
+
+ for idx, offs in enumerate(offsets):
+ geo = build_cube_geo(args.resolution, offs)
+
+ local_args = copy.copy(args)
+ local_args.young_modulus = args.youngs[idx]
+ local_args.poisson_ratio = args.poissons[idx]
+ local_args.density = args.densities[idx]
+
+ sim = ClassicFEM(geo, None, local_args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ # Clamp bottom face for vertical compression
+ sim.set_boundary_condition(boundary_projector_form=clamp_bottom_face)
+
+ # Force applied at top face centre downward (-Y)
+ top_center = wp.vec3(offs[0] + 0.5, 1.0, offs[1] + 0.5)
+
+ sim.forces.count = 1
+ sim.forces.centers = wp.array([top_center], dtype=wp.vec3)
+ sim.forces.radii = wp.array([0.6], dtype=float)
+ sim.forces.forces = wp.array([wp.vec3(0.0, -args.force, 0.0)], dtype=wp.vec3)
+ sim.update_force_weight()
+
+ sims.append(sim)
+ force_sims.append(sim)
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ recorded = [[] for _ in sims]
+
+ for i, sim in enumerate(sims):
+ recorded[i].append(sim.u_field.space.node_positions().numpy())
+
+ n_frames = args.n_frames if args.n_frames > 0 else 1
+
+ for frame in range(n_frames):
+ if args.ramp:
+ scale = float(frame + 1) / float(n_frames)
+ for sim in force_sims:
+ sim.forces.forces = wp.array(
+ [wp.vec3(0.0, -args.force * scale, 0.0)], dtype=wp.vec3
+ )
+ sim.update_force_weight()
+
+ for i, sim in enumerate(sims):
+ sim.run_frame()
+ pos = (
+ sim.u_field.space.node_positions().numpy()
+ + sim.u_field.dof_values.numpy()
+ )
+ recorded[i].append(pos)
+
+ # per-frame energy report matching stretch_two_cubes.py
+ en_frame = [float(sim.evaluate_energy()[0]) for sim in sims]
+ print(f"Frame {frame+1}/{n_frames} energies: {en_frame}")
+
+ # Compute and print potential energies for both cubes
+ energies = []
+ for sim in sims:
+ E, _ = sim.evaluate_energy()
+ energies.append(float(E))
+
+ print("Potential energies (left_cube, right_cube):", energies)
+
+ if args.ui:
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ ps.init()
+ ps.set_ground_plane_mode("shadow_only")
+
+ # Create cam directory if it doesn't exist
+ os.makedirs("cam", exist_ok=True)
+
+ # Load camera view if it exists
+ cam_file = "cam/compress_two_cubes.json"
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ meshes = []
+ for i, sim in enumerate(sims):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ mesh = ps.register_volume_mesh(
+ f"cube_{i}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=0.6,
+ )
+ meshes.append(mesh)
+
+ current = [0]
+ play = [False]
+ last = [time.time()]
+
+ def _ui():
+ changed, val = psim.SliderInt("frame", current[0], 0, n_frames)
+ if changed:
+ current[0] = val
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[val])
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last[0] = time.time()
+
+ # Camera capture button
+ if psim.Button("Capture Camera View"):
+ # Get current camera view as JSON
+ view_json = ps.get_view_as_json()
+ # Simple filename
+ filename = "cam/compress_two_cubes.json"
+ # Save to file
+ with open(filename, "w") as f:
+ json.dump(json.loads(view_json), f, indent=2)
+ print(f"Camera view saved to {filename}")
+
+ # Load camera view dropdown
+ if os.path.exists("cam"):
+ cam_files = [f for f in os.listdir("cam") if f.endswith(".json")]
+ if cam_files:
+ psim.Text("Load Camera View:")
+ for cam_file in sorted(cam_files):
+ if psim.Button(f"Load {cam_file}"):
+ try:
+ with open(f"cam/{cam_file}", "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ if play[0] and time.time() - last[0] > 0.05:
+ current[0] = (current[0] + 1) % (n_frames + 1)
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[current[0]])
+ last[0] = time.time()
+
+ ps.set_user_callback(_ui)
+ ps.show()
+ else:
+ # Headless mode - save screenshots
+ import polyscope as ps
+
+ ps.init()
+ ps.set_ground_plane_mode("shadow_only")
+
+ # Create output directory
+ output_dir = "outputs/compress_two_cubes"
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Load camera view if it exists
+ cam_file = "cam/compress_two_cubes.json"
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ # Register meshes
+ meshes = []
+ for i, sim in enumerate(sims):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ mesh = ps.register_volume_mesh(
+ f"cube_{i}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=0.6,
+ )
+ meshes.append(mesh)
+
+ # Save 10 evenly distributed frames
+ screenshot_frames = [int(i * n_frames / 9) for i in range(10)]
+ print(f"Saving screenshots for frames: {screenshot_frames}")
+
+ for frame_idx in screenshot_frames:
+ # Update mesh positions for this frame
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[frame_idx])
+
+ # Take screenshot
+ filename = f"{output_dir}/frame_{frame_idx:06d}.png"
+ ps.screenshot(filename)
+ print(f"Screenshot saved: {filename}")
+
+ print(f"All screenshots saved to {output_dir}/")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/simulations/cube_fall.py b/deps/vomp/vomp/fem/simulations/cube_fall.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a98ffc01dc57980080570016ad952960393e295
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/cube_fall.py
@@ -0,0 +1,248 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Field, Sample, Domain, integrand
+
+import sys
+import os
+
+# Add the parent directory to the Python path
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# Re-use utilities and kernels from the existing demo_3d script
+from vomp.fem.fem_examples.mfem.demo_3d import (
+ material_fraction_form,
+ mark_active,
+)
+from vomp.fem.fem_examples.mfem.softbody_sim import (
+ ClassicFEM,
+ run_softbody_sim,
+)
+
+# Optional MFEM variants
+from vomp.fem.fem_examples.mfem.mfem_3d import MFEM_RS_F, MFEM_sF_S
+
+from vomp.fem.fem_examples.mfem.collisions import CollisionHandler
+
+
+def build_geometry(res: int):
+ """Create a unit cube grid with origin at (0,0,0)."""
+ return fem.Grid3D(res=wp.vec3i(res), bounds_lo=wp.vec3(0.0, 0.0, 0.0))
+
+
+def main():
+ wp.init()
+
+ # ------------------------------------------------------------------
+ # 2. Parse generic runtime/simulator-specific arguments
+ # ------------------------------------------------------------------
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--resolution", type=int, default=10, help="Grid resolution per axis"
+ )
+ parser.add_argument(
+ "--ui",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Enable Polyscope viewer",
+ )
+ GroundCollidingSim.add_parser_arguments(parser) # include collision args
+ # Provide more Newton iterations by default for stability
+ parser.set_defaults(
+ n_newton=10,
+ n_frames=100,
+ # Softer material so deformation is visible
+ young_modulus=1e4, # Pa
+ poisson_ratio=0.45,
+ density=500.0,
+ # Collision / ground defaults
+ ground=True,
+ ground_height=0.0,
+ collision_radius=0.05,
+ )
+ args = parser.parse_args()
+
+ # ------------------------------------------------------------------
+ # 3. Build geometry and mark all cells as "active" material
+ # ------------------------------------------------------------------
+ # Start above ground but not too high y โ [2 , 3]
+ geo = fem.Grid3D(
+ res=wp.vec3i(args.resolution),
+ bounds_lo=wp.vec3(0.0, 2.0, 0.0),
+ bounds_hi=wp.vec3(1.0, 3.0, 1.0),
+ )
+
+ active_cells = None # simulate all cells without masking
+
+ # ------------------------------------------------------------------
+ # 4. Create simulator and set boundary conditions (fixed bottom)
+ # ------------------------------------------------------------------
+ # Use GroundCollidingSim with collision handling
+ sim_class = GroundCollidingSim
+ sim = sim_class(geo, active_cells, args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ # set up collision detector
+ sim.init_collision_detector()
+
+ # No displacement boundary conditions โ cube can move freely
+ sim.set_boundary_condition(boundary_projector_form=None)
+
+ # ------------------------------------------------------------------
+ # 5. Run the simulation (headless or with Polyscope)
+ # ------------------------------------------------------------------
+ # Re-enable Polyscope ground plane that run_softbody_sim disables
+ def _viewer_init():
+ import polyscope as ps
+
+ ps.set_ground_plane_mode("tile")
+
+ if args.ui:
+ # ---------------------------------------------
+ # First, run the simulation head-less and store every frame
+ # ---------------------------------------------
+ recorded_frames = []
+
+ def _recorder(pos):
+ recorded_frames.append(pos.copy())
+
+ run_softbody_sim(sim, ui=False, frame_callback=_recorder)
+
+ # ---------------------------------------------
+ # Build Polyscope viewer with slider
+ # ---------------------------------------------
+ import polyscope.imgui as psim
+ import polyscope as ps
+
+ ps.init()
+ ps.set_ground_plane_mode("tile") # plane at y = 0 matches ground_height
+ ps.set_ground_plane_height(0.0)
+
+ # connectivity: try hexes first, else tets
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ mesh = ps.register_volume_mesh(
+ "cube",
+ recorded_frames[0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=1.0,
+ )
+
+ current_idx = [0]
+ play = [False]
+ last_t = [time.time()]
+
+ def _slider_callback():
+ changed, new_val = psim.SliderInt(
+ "frame", current_idx[0], 0, len(recorded_frames) - 1
+ )
+ if changed:
+ current_idx[0] = new_val
+ mesh.update_vertex_positions(recorded_frames[new_val])
+
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last_t[0] = time.time()
+
+ # advance automatically at ~20 fps
+ if play[0] and (time.time() - last_t[0] > 0.05):
+ current_idx[0] = (current_idx[0] + 1) % len(recorded_frames)
+ mesh.update_vertex_positions(recorded_frames[current_idx[0]])
+ last_t[0] = time.time()
+
+ ps.set_user_callback(_slider_callback)
+ ps.show()
+ else:
+ run_softbody_sim(sim, ui=False)
+
+
+class GroundCollidingSim(ClassicFEM):
+ """Classic FEM with ground collision handling using existing CollisionHandler."""
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ # inherit all ClassicFEM args plus collision-related ones
+ ClassicFEM.add_parser_arguments(parser)
+ CollisionHandler.add_parser_arguments(parser)
+
+ # -------------------------------------------------------------
+ # Collision-specific utilities
+ # -------------------------------------------------------------
+ def init_collision_detector(self):
+ """Create a CollisionHandler sampling one particle per mesh node."""
+ # One contact particle per velocity node
+ node_pos = self.u_field.space.node_positions()
+ # Pic quadrature over cells at node positions
+ pic_qp = fem.PicQuadrature(fem.Cells(self.geo), node_pos)
+ pic_qp.domain = self.u_test.domain # ensure the same domain as tests
+
+ self.collision_handler = CollisionHandler(
+ kinematic_meshes=[],
+ cp_cell_indices=pic_qp.cell_indices,
+ cp_cell_coords=pic_qp.particle_coords,
+ sim=self,
+ )
+ # store for later reuse
+ self._cp_quadrature = pic_qp
+
+ # -------------------------------------------------------------
+ # Overrides hooking collision terms into Newton solver
+ # -------------------------------------------------------------
+ def compute_initial_guess(self):
+ self.du_field.dof_values.zero_()
+ self.collision_handler.detect_collisions(self.dt)
+
+ def evaluate_energy(self):
+ E, C = super().evaluate_energy()
+ E = self.collision_handler.add_collision_energy(E)
+ return E, C
+
+ def newton_lhs(self):
+ lhs = super().newton_lhs()
+ self.collision_handler.add_collision_hessian(lhs)
+ fem.dirichlet.project_system_matrix(lhs, self.v_bd_matrix)
+ return lhs
+
+ def newton_rhs(self, tape=None):
+ rhs = super().newton_rhs(tape)
+ self.collision_handler.add_collision_forces(rhs)
+ self._filter_forces(rhs, tape=tape)
+ return rhs
+
+ def prepare_newton_step(self, tape=None):
+ self.collision_handler.prepare_newton_step(self.dt)
+ return super().prepare_newton_step(tape)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/simulations/demo_sparse_voxels.py b/deps/vomp/vomp/fem/simulations/demo_sparse_voxels.py
new file mode 100644
index 0000000000000000000000000000000000000000..27069af210c5f47d93d5c4e80e234550f17cb7f1
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/demo_sparse_voxels.py
@@ -0,0 +1,800 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Domain, Sample, Field
+from warp.fem import integrand, normal
+
+import numpy as np
+import trimesh
+from scipy.spatial import cKDTree
+
+import sys
+import os
+
+# Add the parent directory to the Python path
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM, run_softbody_sim
+from vomp.fem.fem_examples.mfem.mfem_3d import MFEM_RS_F, MFEM_sF_S
+from vomp.fem.fem_examples.mfem.collisions import CollisionHandler
+
+import warp.examples.fem.utils as fem_example_utils
+
+# Demo app
+
+
+class GroundCollidingSim(ClassicFEM):
+ """Classic FEM with ground collision handling using existing CollisionHandler."""
+
+ @staticmethod
+ def add_parser_arguments(parser: argparse.ArgumentParser):
+ # inherit all ClassicFEM args plus collision-related ones
+ ClassicFEM.add_parser_arguments(parser)
+ CollisionHandler.add_parser_arguments(parser)
+
+ # -------------------------------------------------------------
+ # Collision-specific utilities
+ # -------------------------------------------------------------
+ def init_collision_detector(self):
+ """Create a CollisionHandler sampling one particle per mesh node."""
+ # One contact particle per velocity node
+ node_pos = self.u_field.space.node_positions()
+ # Pic quadrature over cells at node positions
+ pic_qp = fem.PicQuadrature(fem.Cells(self.geo), node_pos)
+ pic_qp.domain = self.u_test.domain # ensure the same domain as tests
+
+ self.collision_handler = CollisionHandler(
+ kinematic_meshes=[],
+ cp_cell_indices=pic_qp.cell_indices,
+ cp_cell_coords=pic_qp.particle_coords,
+ sim=self,
+ )
+
+ # store for later reuse
+ self._cp_quadrature = pic_qp
+
+ # -------------------------------------------------------------
+ # Overrides hooking collision terms into Newton solver
+ # -------------------------------------------------------------
+ def compute_initial_guess(self):
+ self.du_field.dof_values.zero_()
+ self.collision_handler.detect_collisions(self.dt)
+
+ def evaluate_energy(self):
+ E, C = super().evaluate_energy()
+ E = self.collision_handler.add_collision_energy(E)
+ return E, C
+
+ def newton_lhs(self):
+ lhs = super().newton_lhs()
+ self.collision_handler.add_collision_hessian(lhs)
+ fem.dirichlet.project_system_matrix(lhs, self.v_bd_matrix)
+ return lhs
+
+ def newton_rhs(self, tape=None):
+ rhs = super().newton_rhs(tape)
+ self.collision_handler.add_collision_forces(rhs)
+ self._filter_forces(rhs, tape=tape)
+ return rhs
+
+ def prepare_newton_step(self, tape=None):
+ self.collision_handler.prepare_newton_step(self.dt)
+ return super().prepare_newton_step(tape)
+
+
+def load_voxels_from_npz(
+ npz_path, scale=1.0, offset=(0.0, 0.0, 0.0), rotate_to_vertical=True
+):
+ """
+ Load voxel centers from an NPZ file.
+
+ Note: Voxels are organized in a 64^3 grid structure with coordinates
+ typically ranging from [-0.5, 0.5] in each dimension.
+ This gives a grid spacing of 1.0/64 = 0.015625 units per voxel.
+
+ Args:
+ npz_path: Path to the NPZ file containing voxel data
+ scale: Scaling factor for voxel positions
+ offset: Offset to apply to voxel positions (x, y, z)
+ rotate_to_vertical: Whether to rotate voxels to vertical orientation
+
+ Returns:
+ wp.array of voxel positions as vec3
+ dict containing material properties per voxel
+ float: recommended voxel size for simulation
+ """
+ try:
+ data = np.load(npz_path)
+ voxel_data = data["voxel_data"]
+
+ # Extract positions
+ positions = np.column_stack([voxel_data["x"], voxel_data["y"], voxel_data["z"]])
+
+ # Rotate to vertical orientation if requested (swap Y and Z axes)
+ if rotate_to_vertical:
+ positions = positions[:, [0, 2, 1]] # X, Z, Y -> X, Y, Z (Z becomes Y)
+ print("Applied rotation: swapped Y and Z axes to make voxels vertical")
+
+ # Apply scaling and offset
+ positions = positions * scale
+ positions[:, 0] += offset[0]
+ positions[:, 1] += offset[1]
+ positions[:, 2] += offset[2]
+
+ # Convert to warp array
+ pts = wp.array(positions, dtype=wp.vec3)
+
+ # Extract material properties
+ materials = {
+ "youngs_modulus": voxel_data["youngs_modulus"],
+ "poissons_ratio": voxel_data["poissons_ratio"],
+ "density": voxel_data["density"],
+ "segment_id": voxel_data["segment_id"],
+ }
+
+ # Calculate grid spacing (assuming 64^3 grid in [-0.5, 0.5] range)
+ grid_size = 64
+ theoretical_spacing = 1.0 / grid_size # 0.015625 in normalized space
+
+ # The actual voxel size in simulation space should account for scaling
+ recommended_voxel_size = theoretical_spacing * scale
+
+ print(f"Loaded {len(positions)} voxels from {npz_path}")
+ print(f"Voxels from 64^3 grid (theoretical max: {grid_size**3} voxels)")
+ print(f"Original grid spacing: {theoretical_spacing:.6f}")
+ print(f"Scaling factor: {scale}")
+ print(f"Recommended voxel size for simulation: {recommended_voxel_size:.6f}")
+ print(
+ f"Position bounds: X[{positions[:, 0].min():.3f}, {positions[:, 0].max():.3f}], "
+ f"Y[{positions[:, 1].min():.3f}, {positions[:, 1].max():.3f}], "
+ f"Z[{positions[:, 2].min():.3f}, {positions[:, 2].max():.3f}]"
+ )
+ print(f"Unique materials: {len(np.unique(materials['segment_id']))}")
+
+ return pts, materials, recommended_voxel_size
+
+ except Exception as e:
+ print(f"Error loading NPZ file {npz_path}: {e}")
+ raise
+
+
+def assign_spatially_varying_materials(sim, voxel_positions, materials, voxel_size):
+ """
+ Assign spatially varying material properties to simulation cells based on voxel data.
+
+ Args:
+ sim: The simulation object
+ voxel_positions: numpy array of voxel positions (N, 3)
+ materials: dict containing material properties per voxel
+ voxel_size: size of voxels for volume computation
+
+ Returns:
+ E_cells: numpy array of Young's modulus values per cell (for visualization)
+ """
+ from scipy.spatial import KDTree
+
+ print(f"\n=== SPATIALLY VARYING MATERIAL ASSIGNMENT ===")
+
+ # Build KD-tree for fast nearest neighbor search
+ voxel_tree = KDTree(voxel_positions)
+
+ # Get simulation cell count and positions
+ n_cells = sim.geo.cell_count()
+ print(f"Number of simulation cells: {n_cells}")
+ print(f"Number of material voxels: {len(voxel_positions)}")
+
+ # Compute cell centers from node positions
+ node_positions = sim.u_field.space.node_positions().numpy()
+ cell_node_indices = sim.u_field.space.topology.element_node_indices().numpy()
+
+ cell_positions = np.zeros((n_cells, 3), dtype=np.float32)
+ for cell_idx in range(n_cells):
+ node_indices = cell_node_indices[cell_idx]
+ cell_nodes = node_positions[node_indices]
+ cell_positions[cell_idx] = np.mean(cell_nodes, axis=0)
+
+ print(
+ f"Cell positions range: min={cell_positions.min(axis=0)}, max={cell_positions.max(axis=0)}"
+ )
+ print("Computing cell centers from node positions")
+
+ # Function to convert Young's modulus and Poisson ratio to Lamรฉ parameters
+ def lame_from_E_nu(E, nu):
+ lam = E * nu / ((1 + nu) * (1 - 2 * nu))
+ mu = E / (2 * (1 + nu))
+ return lam, mu
+
+ # Initialize arrays for material properties
+ lame_np = np.zeros((n_cells, 2), dtype=np.float32)
+ density_np = np.zeros(n_cells, dtype=np.float32)
+
+ # Extract material property arrays
+ youngs_modulus = materials["youngs_modulus"]
+ poissons_ratio = materials["poissons_ratio"]
+ density = materials["density"]
+
+ # Statistics tracking
+ max_distance = 0.0
+ total_distance = 0.0
+
+ print("Assigning materials to simulation cells...")
+
+ # Assign material properties to each simulation cell
+ for cell_idx in range(min(n_cells, len(cell_positions))):
+ cell_center = cell_positions[cell_idx]
+
+ # Find closest voxel
+ distance, closest_voxel_idx = voxel_tree.query(cell_center)
+
+ # Get material properties for this voxel
+ E = youngs_modulus[closest_voxel_idx]
+ nu = poissons_ratio[closest_voxel_idx]
+ rho = density[closest_voxel_idx]
+
+ E /= 2e5
+
+ # Convert to Lamรฉ parameters
+ lam, mu = lame_from_E_nu(E, nu)
+ lame_np[cell_idx, 0] = lam
+ lame_np[cell_idx, 1] = mu
+ density_np[cell_idx] = rho
+
+ # Update statistics
+ max_distance = max(max_distance, distance)
+ total_distance += distance
+
+ # Debug output for first few cells
+ if cell_idx < 5:
+ print(
+ f"Cell {cell_idx}: center={cell_center}, closest_voxel={closest_voxel_idx}, "
+ f"distance={distance:.4f}, E={E:.2e}, nu={nu:.3f}, rho={rho:.1f}"
+ )
+
+ # Handle remaining cells if any
+ if len(cell_positions) < n_cells:
+ print(
+ f"WARNING: {n_cells - len(cell_positions)} cells using default material properties"
+ )
+ # Use default values for remaining cells
+ default_E = youngs_modulus.mean()
+ default_nu = poissons_ratio.mean()
+ default_rho = density.mean()
+ default_lam, default_mu = lame_from_E_nu(default_E, default_nu)
+
+ for cell_idx in range(len(cell_positions), n_cells):
+ lame_np[cell_idx, 0] = default_lam
+ lame_np[cell_idx, 1] = default_mu
+ density_np[cell_idx] = default_rho
+
+ # Verify field size compatibility
+ print(f"Lame field expects {sim.lame_field.dof_values.shape[0]} values")
+ print(f"We have {len(lame_np)} material assignments")
+
+ if sim.lame_field.dof_values.shape[0] != len(lame_np):
+ print(
+ f"ERROR: Field size mismatch! Expected {sim.lame_field.dof_values.shape[0]}, got {len(lame_np)}"
+ )
+
+ if len(lame_np) > sim.lame_field.dof_values.shape[0]:
+ print("Truncating material arrays to match field size")
+ lame_np = lame_np[: sim.lame_field.dof_values.shape[0]]
+ density_np = density_np[: sim.lame_field.dof_values.shape[0]]
+ else:
+ print("Extending material arrays to match field size")
+ additional_entries = sim.lame_field.dof_values.shape[0] - len(lame_np)
+
+ # Use mean values for extension
+ mean_lam = lame_np[:, 0].mean()
+ mean_mu = lame_np[:, 1].mean()
+ mean_rho = density_np.mean()
+
+ lame_extension = np.tile([mean_lam, mean_mu], (additional_entries, 1))
+ density_extension = np.full(additional_entries, mean_rho)
+
+ lame_np = np.vstack([lame_np, lame_extension])
+ density_np = np.hstack([density_np, density_extension])
+
+ # Assign to simulation
+ sim.lame_field.dof_values.assign(wp.array(lame_np, dtype=wp.vec2))
+
+ # Store density array for potential future use (currently not used in simulation)
+ sim._voxel_density = density_np
+
+ # Compute final statistics
+ E_cells = (lame_np[:, 1] * (3 * lame_np[:, 0] + 2 * lame_np[:, 1])) / (
+ lame_np[:, 0] + lame_np[:, 1] + 1e-9
+ )
+
+ print(f"\n=== MATERIAL ASSIGNMENT RESULTS ===")
+ print(f"Maximum distance to closest voxel: {max_distance:.4f}")
+ print(f"Average distance to closest voxel: {total_distance / n_cells:.4f}")
+ print(f"Final Young's modulus range: {E_cells.min():.2e} - {E_cells.max():.2e} Pa")
+ print(
+ f"Final Poisson's ratio range: {poissons_ratio.min():.3f} - {poissons_ratio.max():.3f}"
+ )
+ print(f"Final density range: {density_np.min():.1f} - {density_np.max():.1f} kg/mยณ")
+
+ # Check for potential issues
+ zero_lam = np.sum(lame_np[:, 0] == 0)
+ zero_mu = np.sum(lame_np[:, 1] == 0)
+ if zero_lam > 0 or zero_mu > 0:
+ print(
+ f"WARNING: Found {zero_lam} cells with zero lambda, {zero_mu} cells with zero mu"
+ )
+
+ print("Spatially varying material assignment completed successfully!\n")
+
+ return E_cells # Return for visualization purposes
+
+
+def load_ply_mesh(
+ ply_path, scale=1.0, offset=(0.0, 0.0, 0.0), flip_y=False, flip_z=False
+):
+ """
+ Load a PLY mesh file for visualization.
+
+ Args:
+ ply_path: Path to the PLY file
+ scale: Scaling factor for mesh positions
+ offset: Offset to apply to mesh positions (x, y, z)
+ flip_y: Whether to flip Y axis
+ flip_z: Whether to flip Z axis
+
+ Returns:
+ vertices: np.array of vertex positions
+ faces: np.array of face indices
+ """
+ try:
+ mesh = trimesh.load_mesh(ply_path)
+
+ # Get vertices and faces
+ vertices = mesh.vertices.copy()
+ faces = mesh.faces.copy()
+
+ # Apply coordinate transformations
+ if flip_y:
+ vertices[:, 1] = -vertices[:, 1]
+ if flip_z:
+ vertices[:, 2] = -vertices[:, 2]
+
+ # Apply scaling and offset
+ vertices = vertices * scale
+ vertices[:, 0] += offset[0]
+ vertices[:, 1] += offset[1]
+ vertices[:, 2] += offset[2]
+
+ print(f"Loaded PLY mesh from {ply_path}")
+ print(f"Vertices: {len(vertices)}, Faces: {len(faces)}")
+ print(f"Applied transformations: flip_y={flip_y}, flip_z={flip_z}")
+ print(
+ f"Vertex bounds: X[{vertices[:, 0].min():.3f}, {vertices[:, 0].max():.3f}], "
+ f"Y[{vertices[:, 1].min():.3f}, {vertices[:, 1].max():.3f}], "
+ f"Z[{vertices[:, 2].min():.3f}, {vertices[:, 2].max():.3f}]"
+ )
+
+ return vertices, faces
+
+ except Exception as e:
+ print(f"Error loading PLY file {ply_path}: {e}")
+ raise
+
+
+def compute_barycentric_mapping(ply_vertices, voxel_positions):
+ """
+ Compute barycentric mapping from PLY mesh vertices to voxel tetrahedral mesh.
+
+ Args:
+ ply_vertices: PLY mesh vertex positions (N, 3)
+ voxel_positions: Voxel mesh vertex positions (M, 3)
+
+ Returns:
+ mapping_data: Dictionary containing mapping information for deformation
+ """
+ print("Computing barycentric mapping from PLY mesh to voxel mesh...")
+
+ # Build KD-tree for fast nearest neighbor search
+ tree = cKDTree(voxel_positions)
+
+ # For each PLY vertex, find closest voxel vertices for interpolation
+ ply_to_voxel_mapping = []
+
+ for i, ply_vertex in enumerate(ply_vertices):
+ # Find k nearest voxel vertices
+ k = min(4, len(voxel_positions)) # Use up to 4 nearest neighbors
+ distances, indices = tree.query(ply_vertex, k=k)
+
+ # Compute inverse distance weights (avoid division by zero)
+ weights = 1.0 / (distances + 1e-8)
+ weights = weights / np.sum(weights) # Normalize weights
+
+ ply_to_voxel_mapping.append(
+ {
+ "indices": indices,
+ "weights": weights,
+ }
+ )
+
+ print(f"Computed mapping for {len(ply_vertices)} PLY vertices to voxel mesh")
+
+ return {
+ "ply_to_voxel": ply_to_voxel_mapping,
+ "original_ply_vertices": ply_vertices.copy(),
+ "original_voxel_positions": voxel_positions.copy(),
+ }
+
+
+def deform_ply_mesh(mapping_data, current_voxel_positions):
+ """
+ Deform PLY mesh based on current voxel positions using precomputed mapping.
+
+ Args:
+ mapping_data: Mapping data from compute_barycentric_mapping
+ current_voxel_positions: Current deformed voxel positions (M, 3)
+
+ Returns:
+ deformed_ply_vertices: Deformed PLY mesh vertices (N, 3)
+ """
+ original_ply = mapping_data["original_ply_vertices"]
+ original_voxel = mapping_data["original_voxel_positions"]
+ ply_to_voxel = mapping_data["ply_to_voxel"]
+
+ deformed_ply = np.zeros_like(original_ply)
+
+ for i, mapping in enumerate(ply_to_voxel):
+ indices = mapping["indices"]
+ weights = mapping["weights"]
+
+ # Compute weighted displacement from original to current voxel positions
+ original_voxel_cluster = original_voxel[indices]
+ current_voxel_cluster = current_voxel_positions[indices]
+
+ # Displacement of each voxel vertex
+ displacements = current_voxel_cluster - original_voxel_cluster
+
+ # Weighted average displacement
+ avg_displacement = np.sum(weights[:, np.newaxis] * displacements, axis=0)
+
+ # Apply displacement to PLY vertex
+ deformed_ply[i] = original_ply[i] + avg_displacement
+
+ return deformed_ply
+
+
+if __name__ == "__main__":
+ # wp.config.verify_cuda = True
+ # wp.config.verify_fp = True
+ wp.init()
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--npz_file", "-f", type=str, help="Path to NPZ file containing voxel data"
+ )
+ parser.add_argument(
+ "--ply_file", "-p", type=str, help="Path to PLY file for visualization mesh"
+ )
+ parser.add_argument(
+ "--voxel_size",
+ "-dx",
+ type=float,
+ default=0.015625,
+ help="Voxel size for simulation. When loading NPZ files, this will be auto-calculated based on scale unless explicitly overridden.",
+ )
+ parser.add_argument(
+ "--scale", type=float, default=2.0, help="Scaling factor for voxel positions"
+ )
+ parser.add_argument(
+ "--offset_y",
+ type=float,
+ default=1.5,
+ help="Y offset to lift object above ground",
+ )
+ parser.add_argument(
+ "--flip_ply_y",
+ action="store_true",
+ help="Don't flip Y axis of PLY mesh (Y axis is flipped by default to match voxel coordinates)",
+ )
+ parser.add_argument(
+ "--flip_ply_z", action="store_true", help="Flip Z axis of PLY mesh"
+ )
+ parser.add_argument(
+ "--no_rotate_voxels",
+ action="store_true",
+ help="Don't rotate voxels to vertical orientation",
+ )
+ parser.add_argument(
+ "--static_reference",
+ action="store_true",
+ help="Keep PLY mesh static (don't deform with voxels)",
+ )
+ parser.add_argument("--grid", action=argparse.BooleanOptionalAction)
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+ GroundCollidingSim.add_parser_arguments(parser)
+ args = parser.parse_args()
+
+ # Force 100 frames for the headless simulation
+ args.n_frames = 50
+
+ # Increase Newton iterations for better convergence
+ args.n_newton = 10
+
+ # Load PLY mesh for visualization if provided
+ ply_vertices = None
+ ply_faces = None
+ if args.ply_file:
+ ply_vertices, ply_faces = load_ply_mesh(
+ args.ply_file,
+ scale=args.scale,
+ offset=(0.0, args.offset_y, 0.0),
+ flip_y=not args.flip_ply_y, # Flip Y by default to match voxel coordinate system
+ flip_z=args.flip_ply_z,
+ )
+
+ if args.npz_file:
+ # Load voxels from NPZ file
+ pts, materials, recommended_voxel_size = load_voxels_from_npz(
+ args.npz_file,
+ scale=args.scale,
+ offset=(0.0, args.offset_y, 0.0),
+ rotate_to_vertical=not args.no_rotate_voxels,
+ )
+
+ # Use recommended voxel size unless user specified a custom one
+ if args.voxel_size == 0.015625: # Default value, use recommended
+ voxel_size = recommended_voxel_size
+ print(f"Using recommended voxel size: {voxel_size:.6f}")
+ else: # User specified custom voxel size
+ voxel_size = args.voxel_size
+ print(f"Using user-specified voxel size: {voxel_size:.6f}")
+ else:
+ # Fallback to original random sphere generation
+ n_pts = 1000
+ print("No NPZ file specified, generating random sphere points...")
+
+ # generate random points on a sphere
+ pts = np.random.rand(n_pts, 3) * 2.0 - 1.0
+ pts /= 2.0 * np.linalg.norm(pts, axis=1)[:, None] + 0.001
+ pts[:, 1] += 1.5
+ pts = wp.array(pts, dtype=wp.vec3)
+ materials = None
+ voxel_size = args.voxel_size
+
+ volume = wp.Volume.allocate_by_voxels(pts, voxel_size=voxel_size)
+ geo = fem.Nanogrid(volume)
+
+ sim = GroundCollidingSim(geo, active_cells=None, args=args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+ sim.init_collision_detector()
+
+ sim.set_boundary_condition(
+ boundary_projector_form=None,
+ )
+
+ # Assign spatially varying materials if NPZ file was loaded
+ E_cells = None
+ if args.npz_file and materials is not None:
+ voxel_positions_np = pts.numpy()
+ E_cells = assign_spatially_varying_materials(
+ sim, voxel_positions_np, materials, voxel_size
+ )
+
+ # Update typical stiffness based on actual material properties
+ max_E = float(E_cells.max())
+ min_E = float(E_cells.min())
+ # Use a moderate stiffness to balance stability and deformation
+ sim.typical_stiffness = max(
+ args.density * args.gravity * sim.typical_length,
+ min(
+ min_E * 0.01, args.density * sim.typical_length**2 / (args.dt**2)
+ ), # Use 1% of min stiffness
+ )
+ print(
+ f"Updated typical stiffness to: {sim.typical_stiffness:.2e} based on material range {min_E:.2e} - {max_E:.2e}"
+ )
+ print(f"Using reduced stiffness (1% of minimum) for better deformation")
+
+ if args.ui:
+ # First run the simulation headless and record all frames
+ print(f"Running simulation for {args.n_frames} frames...")
+ recorded_frames = []
+
+ def frame_recorder(displaced_pos):
+ recorded_frames.append(displaced_pos.copy())
+
+ # Run headless simulation
+ run_softbody_sim(sim, ui=False, frame_callback=frame_recorder)
+
+ print(f"Simulation complete! Recorded {len(recorded_frames)} frames.")
+
+ # Compute PLY-to-voxel mapping if both meshes are available
+ mapping_data = None
+ if ply_vertices is not None and not args.static_reference:
+ print("Computing mesh deformation mapping...")
+ initial_voxel_positions = recorded_frames[0]
+ mapping_data = compute_barycentric_mapping(
+ ply_vertices, initial_voxel_positions
+ )
+ print("Mesh deformation mapping complete!")
+
+ print("Starting playback visualization...")
+
+ # Now set up the UI for playback
+ import polyscope as ps
+ import polyscope.imgui as psim
+ import time
+
+ ps.init()
+ ps.set_ground_plane_mode("tile")
+ ps.set_ground_plane_height(0.0)
+
+ # Get voxel mesh connectivity
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ # Always register the voxel mesh (this is what actually deforms)
+ voxel_mesh = ps.register_volume_mesh(
+ "voxel_simulation",
+ recorded_frames[0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=1.0,
+ enabled=True, # Always show by default
+ )
+
+ # Register PLY mesh if provided
+ ply_mesh = None
+ deformed_ply_frames = []
+ if ply_vertices is not None and ply_faces is not None:
+ if mapping_data is not None:
+ # Compute deformed PLY mesh for all frames
+ print("Computing deformed PLY mesh for all frames...")
+ for i, voxel_frame in enumerate(recorded_frames):
+ deformed_ply = deform_ply_mesh(mapping_data, voxel_frame)
+ deformed_ply_frames.append(deformed_ply)
+ if i % 10 == 0:
+ print(f"Processed frame {i+1}/{len(recorded_frames)}")
+
+ print("Registering deformable PLY mesh")
+ ply_mesh = ps.register_surface_mesh(
+ "deforming_mesh", deformed_ply_frames[0], ply_faces, enabled=True
+ )
+ else:
+ # Static PLY mesh
+ print("Registering static PLY mesh")
+ ply_mesh = ps.register_surface_mesh(
+ "reference_mesh", ply_vertices, ply_faces, enabled=True
+ )
+ # Make it semi-transparent to see the voxels
+ ply_mesh.set_transparency(0.5)
+
+ # Playback controls
+ current_frame = [0]
+ is_playing = [False]
+ last_time = [time.time()]
+ playback_speed = [1.0]
+ show_reference = [ply_mesh is not None]
+ show_voxels = [True]
+
+ def ui_callback():
+ # Frame controls
+ psim.TextUnformatted(
+ f"Frame: {current_frame[0] + 1} / {len(recorded_frames)}"
+ )
+
+ # Slider for manual frame control
+ changed, new_frame = psim.SliderInt(
+ "##frame", current_frame[0], 0, len(recorded_frames) - 1
+ )
+ if changed:
+ current_frame[0] = new_frame
+ voxel_mesh.update_vertex_positions(recorded_frames[current_frame[0]])
+
+ # Update PLY mesh if it's deformable
+ if deformed_ply_frames:
+ ply_mesh.update_vertex_positions(
+ deformed_ply_frames[current_frame[0]]
+ )
+
+ # Play/Pause button
+ if psim.Button("Play" if not is_playing[0] else "Pause"):
+ is_playing[0] = not is_playing[0]
+ last_time[0] = time.time()
+
+ psim.SameLine()
+
+ # Reset button
+ if psim.Button("Reset"):
+ current_frame[0] = 0
+ is_playing[0] = False
+ voxel_mesh.update_vertex_positions(recorded_frames[0])
+ if deformed_ply_frames:
+ ply_mesh.update_vertex_positions(deformed_ply_frames[0])
+
+ # Speed control
+ changed, new_speed = psim.SliderFloat("Speed", playback_speed[0], 0.1, 5.0)
+ if changed:
+ playback_speed[0] = new_speed
+
+ # Toggle mesh visibility
+ if ply_mesh is not None:
+ changed, new_show = psim.Checkbox(
+ "Show Surface Mesh", show_reference[0]
+ )
+ if changed:
+ show_reference[0] = new_show
+ ply_mesh.set_enabled(show_reference[0])
+
+ # Toggle voxel visibility
+ changed, new_show = psim.Checkbox("Show Voxel Mesh", show_voxels[0])
+ if changed:
+ show_voxels[0] = new_show
+ voxel_mesh.set_enabled(show_voxels[0])
+
+ # Display mesh deformation info
+ if mapping_data is not None:
+ psim.TextUnformatted("Surface mesh: DEFORMING with voxels")
+ elif ply_mesh is not None:
+ psim.TextUnformatted("Surface mesh: STATIC reference")
+
+ # Display material property information (if available)
+ if E_cells is not None:
+ psim.Separator()
+ psim.TextUnformatted("Material Properties:")
+ psim.TextUnformatted(
+ f"Young's Modulus: {E_cells.min():.2e} - {E_cells.max():.2e} Pa"
+ )
+ if hasattr(sim, "_voxel_density"):
+ density_range = sim._voxel_density
+ psim.TextUnformatted(
+ f"Density: {density_range.min():.1f} - {density_range.max():.1f} kg/mยณ"
+ )
+ psim.TextUnformatted(
+ f"Material variation: {E_cells.max()/E_cells.min():.1f}x stiffness range"
+ )
+
+ # Auto-advance frames if playing
+ if is_playing[0]:
+ current_time = time.time()
+ dt = current_time - last_time[0]
+ if dt >= (1.0 / 30.0) / playback_speed[0]: # 30 FPS base rate
+ current_frame[0] = (current_frame[0] + 1) % len(recorded_frames)
+ voxel_mesh.update_vertex_positions(
+ recorded_frames[current_frame[0]]
+ )
+ if deformed_ply_frames:
+ ply_mesh.update_vertex_positions(
+ deformed_ply_frames[current_frame[0]]
+ )
+ last_time[0] = current_time
+
+ ps.set_user_callback(ui_callback)
+ ps.show()
+ else:
+ # Run headless only
+ run_softbody_sim(sim, ui=False)
diff --git a/deps/vomp/vomp/fem/simulations/grid_cubes_fall.py b/deps/vomp/vomp/fem/simulations/grid_cubes_fall.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab6571b35cd2af94c8941d0385864d51f620ef2c
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/grid_cubes_fall.py
@@ -0,0 +1,501 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+import copy
+import json
+import os
+import numpy as np
+import warp as wp
+import warp.fem as fem
+from vomp.fem.simulations.cube_fall import GroundCollidingSim, run_softbody_sim
+from vomp.fem.simulations.material_grids_generated import (
+ get_material_properties_setting_1,
+ get_material_properties_setting_2,
+ get_material_properties_setting_3,
+)
+
+
+def build_cube_geo(res, offset, height=2.0):
+ """Return a Grid3D geometry for one cube centred at offset (x,z)."""
+ lo = wp.vec3(offset[0], height, offset[1])
+ hi = wp.vec3(offset[0] + 1.0, height + 1.0, offset[1] + 1.0)
+ return fem.Grid3D(res=wp.vec3i(res), bounds_lo=lo, bounds_hi=hi)
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser(
+ description="Grid of falling cubes with different material properties"
+ )
+ parser.add_argument("--resolution", type=int, default=8)
+ parser.add_argument("--grid_n", type=int, default=5, help="NxN cubes")
+ parser.add_argument(
+ "--spacing", type=float, default=1.5, help="center spacing between cubes"
+ )
+ parser.add_argument(
+ "--drop_height", type=float, default=2.5, help="initial height of cubes"
+ )
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+
+ # Material property setting
+ parser.add_argument(
+ "--setting",
+ type=int,
+ default=1,
+ choices=[1, 2, 3],
+ help="Material property setting: 1 (YM=1e4), 2 (YM=1e5), 3 (YM=1e6)",
+ )
+
+ # View selection for headless mode
+ parser.add_argument(
+ "--view",
+ type=int,
+ choices=[1, 2],
+ help="Camera view to use in headless mode: 1 or 2. If not specified, uses all available views.",
+ )
+
+ # Add GroundCollidingSim arguments
+ GroundCollidingSim.add_parser_arguments(parser)
+
+ # Set some defaults suitable for falling cubes
+ parser.set_defaults(
+ n_frames=200,
+ gravity=9.81,
+ quasi_quasistatic=False,
+ young_modulus=1e4, # will be overridden per cube
+ poisson_ratio=0.45, # will be overridden per cube
+ density=1000.0, # will be overridden per cube
+ # Collision / ground defaults - important for proper collision!
+ ground=True,
+ ground_height=0.0,
+ collision_radius=0.05,
+ n_newton=10, # More iterations for stability with collision
+ )
+
+ args = parser.parse_args()
+
+ # Get pre-computed material properties for the selected setting
+ setting_functions = {
+ 1: get_material_properties_setting_1,
+ 2: get_material_properties_setting_2,
+ 3: get_material_properties_setting_3,
+ }
+
+ materials_grid = setting_functions[args.setting]()
+
+ # Convert 2D grid to flat arrays
+ n_cubes = args.grid_n * args.grid_n
+ youngs = np.zeros(n_cubes)
+ poissons = np.zeros(n_cubes)
+ densities = np.zeros(n_cubes)
+
+ cube_idx = 0
+ for iz in range(args.grid_n):
+ for ix in range(args.grid_n):
+ ym, poisson, density = materials_grid[iz][ix]
+ youngs[cube_idx] = ym
+ poissons[cube_idx] = poisson
+ densities[cube_idx] = density
+ cube_idx += 1
+
+ print(f"Using pre-computed material property setting {args.setting}")
+ print(f"Creating {n_cubes} cubes with interpolated material properties:")
+ print(
+ f"Middle cube (setting {args.setting}): Young's modulus = {[5e4, 5e5, 1e6][args.setting-1]:.0e}"
+ )
+ print(f"Young's modulus range: {youngs.min():.0e} - {youngs.max():.0e}")
+ print(f"Poisson ratio range: {poissons.min():.3f} - {poissons.max():.3f}")
+ print(f"Density range: {densities.min():.0f} - {densities.max():.0f}")
+ print()
+
+ sims = []
+ cube_idx = 0
+
+ for ix in range(args.grid_n):
+ for iz in range(args.grid_n):
+ offset = (ix * args.spacing, iz * args.spacing)
+ geo = build_cube_geo(args.resolution, offset, height=args.drop_height)
+
+ # Create local args with specific material properties for this cube
+ local_args = copy.copy(args)
+ local_args.young_modulus = youngs[cube_idx]
+ local_args.poisson_ratio = poissons[cube_idx]
+ local_args.density = densities[cube_idx]
+
+ sim = GroundCollidingSim(geo, None, local_args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+ sim.init_collision_detector()
+ sim.set_boundary_condition(boundary_projector_form=None)
+
+ # allocate constant matrices needed by Newton solver
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ sims.append(sim)
+ cube_idx += 1
+
+ # record frames for all sims
+ n_frames = args.n_frames if hasattr(args, "n_frames") and args.n_frames > 0 else 100
+ recorded = [[] for _ in sims]
+
+ # record initial rest pose
+ for i, sim in enumerate(sims):
+ recorded[i].append(
+ sim.u_field.space.node_positions().numpy() + sim.u_field.dof_values.numpy()
+ )
+
+ # Run simulation
+ for frame in range(n_frames):
+ for i, sim in enumerate(sims):
+ sim.run_frame()
+ recorded[i].append(
+ sim.u_field.space.node_positions().numpy()
+ + sim.u_field.dof_values.numpy()
+ )
+
+ # Print progress every 20 frames
+ if frame % 20 == 0:
+ print(f"Frame {frame+1}/{n_frames}")
+
+ if args.ui:
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ ps.init()
+ ps.set_window_size(1920, 1080)
+ ps.set_ground_plane_mode("shadow_only")
+ ps.set_ground_plane_height(0.0)
+
+ # Create cam directory if it doesn't exist
+ os.makedirs("cam", exist_ok=True)
+
+ # Load camera view if it exists - try view1 first, then fallback to old format
+ cam_file = "cam/grid_cubes_fall_view1.json"
+ if not os.path.exists(cam_file):
+ cam_file = "cam/grid_cubes_fall.json"
+
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ meshes = []
+ for i, sim in enumerate(sims):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+ m = ps.register_volume_mesh(
+ f"cube_{i}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=0.6,
+ )
+ meshes.append(m)
+
+ current = [0]
+ play = [False]
+ last = [time.time()]
+
+ def _ui():
+ changed, val = psim.SliderInt("frame", current[0], 0, n_frames - 1)
+ if changed:
+ current[0] = val
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[val])
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last[0] = time.time()
+
+ # Two camera capture buttons
+ if psim.Button("Capture Camera View 1"):
+ # Get current camera view as JSON
+ view_json = ps.get_view_as_json()
+ # Save to first camera file
+ filename = "cam/grid_cubes_fall_view1.json"
+ # Save to file
+ with open(filename, "w") as f:
+ json.dump(json.loads(view_json), f, indent=2)
+ print(f"Camera view 1 saved to {filename}")
+
+ psim.SameLine()
+ if psim.Button("Capture Camera View 2"):
+ # Get current camera view as JSON
+ view_json = ps.get_view_as_json()
+ # Save to second camera file
+ filename = "cam/grid_cubes_fall_view2.json"
+ # Save to file
+ with open(filename, "w") as f:
+ json.dump(json.loads(view_json), f, indent=2)
+ print(f"Camera view 2 saved to {filename}")
+
+ # Load camera view dropdown
+ if os.path.exists("cam"):
+ cam_files = [f for f in os.listdir("cam") if f.endswith(".json")]
+ if cam_files:
+ psim.Text("Load Camera View:")
+ for cam_file in sorted(cam_files):
+ if psim.Button(f"Load {cam_file}"):
+ try:
+ with open(f"cam/{cam_file}", "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ # Material properties display
+ psim.Text("Material Properties:")
+ psim.Text(
+ f"Setting: {args.setting} (Middle cube YM = {[5e4, 5e5, 1e6][args.setting-1]:.0e})"
+ )
+ psim.Text(f"Young's modulus: {youngs.min():.0e} - {youngs.max():.0e}")
+ psim.Text(f"Poisson ratio: {poissons.min():.3f} - {poissons.max():.3f}")
+ psim.Text(f"Density: {densities.min():.0f} - {densities.max():.0f}")
+ psim.Text(f"Total cubes: {len(sims)}")
+ psim.Text("Properties interpolated from middle cube outward")
+
+ if play[0] and time.time() - last[0] > 0.05:
+ current[0] = (current[0] + 1) % n_frames
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[current[0]])
+ last[0] = time.time()
+
+ ps.set_user_callback(_ui)
+ ps.show()
+ else:
+ # Headless mode - save screenshots
+ import polyscope as ps
+ import subprocess
+ import glob
+
+ print(f"\nRunning headless mode for material setting {args.setting}")
+ print(f"Middle cube Young's modulus: {[5e4, 5e5, 1e6][args.setting-1]:.0e} Pa")
+
+ ps.init()
+ ps.set_ground_plane_mode("shadow_only")
+ ps.set_ground_plane_height(0.0)
+
+ # Create output directory with setting subfolder
+ output_dir = f"outputs/grid_cubes_fall/setting_{args.setting}"
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Check which camera views exist and filter by --view argument if specified
+ all_camera_views = []
+ for view_name in ["view1", "view2"]:
+ cam_file = f"cam/grid_cubes_fall_{view_name}.json"
+ if os.path.exists(cam_file):
+ all_camera_views.append((view_name, cam_file))
+
+ # Fallback to old format if no new views exist
+ if not all_camera_views:
+ cam_file = "cam/grid_cubes_fall.json"
+ if os.path.exists(cam_file):
+ all_camera_views.append(("default", cam_file))
+
+ # Filter views based on --view argument
+ if args.view is not None:
+ view_name = f"view{args.view}"
+ cam_file = f"cam/grid_cubes_fall_{view_name}.json"
+ if os.path.exists(cam_file):
+ camera_views = [(view_name, cam_file)]
+ print(f"Using specified view {args.view}")
+ else:
+ print(
+ f"Warning: View {args.view} not found ({cam_file}). Available views:"
+ )
+ for name, file in all_camera_views:
+ print(f" - {name}: {file}")
+ camera_views = []
+ else:
+ camera_views = all_camera_views
+
+ if not camera_views:
+ print("No camera view files found. Using default camera.")
+ camera_views.append(("default", None))
+
+ # Register meshes once
+ meshes = []
+ for i, sim in enumerate(sims):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ m = ps.register_volume_mesh(
+ f"cube_{i}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=0.6,
+ )
+ meshes.append(m)
+
+ # Process each camera view
+ for view_name, cam_file in camera_views:
+ print(f"\nProcessing camera view: {view_name}")
+
+ # Load camera view
+ if cam_file and os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ # Create subdirectory for this view
+ view_output_dir = f"{output_dir}/{view_name}"
+ frames_dir = f"{view_output_dir}/frames"
+ os.makedirs(frames_dir, exist_ok=True)
+
+ # Save 5 screenshots at specific frames
+ screenshot_frames = [0, 20, 30, 50, 199]
+ # Ensure all frames are within bounds
+ screenshot_frames = [f for f in screenshot_frames if f < n_frames]
+ print(
+ f"Saving {len(screenshot_frames)} key screenshots for {view_name} at frames: {screenshot_frames}"
+ )
+
+ if len(screenshot_frames) < 5:
+ print(
+ f"Warning: Only {len(screenshot_frames)} frames available (n_frames={n_frames}). Consider increasing --n_frames to at least 200."
+ )
+
+ for i, frame_idx in enumerate(screenshot_frames):
+ # Update mesh positions for this frame
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[frame_idx])
+
+ # Take screenshot
+ filename = (
+ f"{view_output_dir}/key_frame_{i+1:02d}_frame_{frame_idx:06d}.png"
+ )
+ ps.screenshot(filename)
+ print(f"Key screenshot saved: {filename}")
+
+ # Save all frames for video creation
+ print(f"Saving all {n_frames} frames for video creation...")
+
+ for frame_idx in range(n_frames):
+ # Update mesh positions for this frame
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[frame_idx])
+
+ # Take screenshot for video
+ filename = f"{frames_dir}/frame_{frame_idx:06d}.png"
+ ps.screenshot(filename)
+
+ # Progress indicator
+ if frame_idx % 10 == 0:
+ print(f" Frame {frame_idx+1}/{n_frames}")
+
+ # Create video using ffmpeg
+ video_filename = f"{view_output_dir}/animation_{view_name}.mp4"
+ ffmpeg_cmd = [
+ "ffmpeg",
+ "-y", # -y to overwrite output file
+ "-framerate",
+ "20", # 20 FPS
+ "-i",
+ f"{frames_dir}/frame_%06d.png",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-crf",
+ "18", # High quality
+ video_filename,
+ ]
+
+ try:
+ print(f"Creating video: {video_filename}")
+ subprocess.run(ffmpeg_cmd, check=True, capture_output=True)
+ print(f"Video created successfully: {video_filename}")
+
+ # Delete frame images after successful video creation
+ frame_files = glob.glob(f"{frames_dir}/*.png")
+ for frame_file in frame_files:
+ os.remove(frame_file)
+ os.rmdir(frames_dir)
+ print(f"Cleaned up {len(frame_files)} frame images")
+
+ except subprocess.CalledProcessError as e:
+ print(f"Error creating video with ffmpeg: {e}")
+ print(f"Frame images preserved in: {frames_dir}")
+ except FileNotFoundError:
+ print("ffmpeg not found. Please install ffmpeg to create videos.")
+ print(f"Frame images saved in: {frames_dir}")
+ print("You can manually create the video with:")
+ print(
+ f" ffmpeg -framerate 20 -i {frames_dir}/frame_%06d.png -c:v libx264 -pix_fmt yuv420p {video_filename}"
+ )
+
+ print(f"\nAll outputs saved to {output_dir}/")
+ print(
+ f"Material setting {args.setting}: {[5e4, 5e5, 1e6][args.setting-1]:.0e} Pa Young's modulus"
+ )
+ if args.view:
+ print(f"Processed view {args.view} with {n_frames} frames")
+ else:
+ print(
+ f"Processed {len(camera_views)} camera view(s) with {n_frames} frames each"
+ )
+ if camera_views:
+ print("Each view contains:")
+ print(" - Key frame screenshots at frames: 0, 20, 30, 50, 199")
+ print(" - Full animation video (if ffmpeg available)")
+ print(
+ f"Structure: outputs/grid_cubes_fall/setting_{args.setting}/[view_name]/"
+ )
+
+ # Show which views were processed
+ if camera_views:
+ print("Views processed:")
+ for view_name, cam_file in camera_views:
+ print(f" - {view_name}: {cam_file if cam_file else 'default camera'}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/simulations/interp.py b/deps/vomp/vomp/fem/simulations/interp.py
new file mode 100644
index 0000000000000000000000000000000000000000..77798cb06c8b87a03472949a7c9fe15483d9a9e9
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/interp.py
@@ -0,0 +1,574 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+import copy
+import json
+import os
+import numpy as np
+import warp as wp
+import warp.fem as fem
+from vomp.fem.simulations.cube_fall import GroundCollidingSim, run_softbody_sim
+
+
+def load_material_interpolation_data():
+ """Load material interpolation data from JSON file."""
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ json_path = os.path.join(script_dir, "material_interpolation_results.json")
+
+ with open(json_path, "r") as f:
+ data = json.load(f)
+
+ return data
+
+
+def build_cube_geo(res, x_offset, height=2.0, width=1.0):
+ """Return a Grid3D geometry for one cube at x_offset."""
+ lo = wp.vec3(x_offset, height, 0.0)
+ hi = wp.vec3(x_offset + width, height + width, width)
+ return fem.Grid3D(res=wp.vec3i(res), bounds_lo=lo, bounds_hi=hi)
+
+
+def write_material_properties_summary(
+ output_dir, args, material_1, material_2, cube_materials
+):
+ """Write a detailed summary of material properties and color coding to a text file."""
+ summary_file = os.path.join(
+ output_dir, f"material_properties_summary_pair_{args.pair}.txt"
+ )
+
+ with open(summary_file, "w") as f:
+ f.write("MATERIAL INTERPOLATION SIMULATION SUMMARY\n")
+ f.write("=" * 50 + "\n\n")
+
+ f.write(f"Material Pair {args.pair}:\n")
+ f.write(f" Base Material 1: {material_1['name']}\n")
+ f.write(f" Base Material 2: {material_2['name']}\n")
+ f.write(f" Simulation Frames: {args.n_frames}\n")
+ f.write(f" Cube Spacing: {args.spacing} units\n")
+ f.write(f" Drop Height: {args.drop_height} units\n\n")
+
+ f.write("CUBE ARRANGEMENT (Left to Right):\n")
+ f.write("-" * 50 + "\n")
+
+ for i, mat in enumerate(cube_materials):
+ position = i + 1
+
+ if i == 0:
+ color = "Green"
+ material_type = "Original Material 1"
+ elif i == 4:
+ color = "Yellow"
+ material_type = "Original Material 2"
+ else:
+ color = f"Blue (intensity {0.3 + 0.5 * (i-1)/2:.1f})"
+ interp_factor = [0.25, 0.5, 0.75][i - 1]
+ material_type = f"Interpolated ({interp_factor:.2f} factor)"
+
+ f.write(f"Position {position}: {mat['name']}\n")
+ f.write(f" Material Type: {material_type}\n")
+ f.write(f" Visualization Color: {color}\n")
+ f.write(f" Young's Modulus: {mat['youngs_modulus_gpa']:.6f} GPa\n")
+ f.write(f" Poisson Ratio: {mat['poisson_ratio']:.6f}\n")
+ f.write(f" Density: {mat['density']:.2f} kg/mยณ\n")
+ f.write(
+ f" Young's Modulus (Pa): {mat['youngs_modulus_gpa'] * 1e9:.0f} Pa\n"
+ )
+ f.write("\n")
+
+ f.write("INTERPOLATION DETAILS:\n")
+ f.write("-" * 50 + "\n")
+ f.write(
+ "The middle three cubes have properties interpolated between the two base materials:\n"
+ )
+ f.write(" โข Position 2: 25% interpolation (75% Material 1 + 25% Material 2)\n")
+ f.write(" โข Position 3: 50% interpolation (50% Material 1 + 50% Material 2)\n")
+ f.write(
+ " โข Position 4: 75% interpolation (25% Material 1 + 75% Material 2)\n\n"
+ )
+
+ f.write("EXPECTED BEHAVIOR:\n")
+ f.write("-" * 50 + "\n")
+ f.write(
+ "Each cube falls freely under gravity with its specific material properties.\n"
+ )
+ f.write(
+ "Differences in Young's modulus, Poisson ratio, and density will cause:\n"
+ )
+ f.write(" โข Different deformation patterns during impact\n")
+ f.write(" โข Varying bounce characteristics\n")
+ f.write(" โข Different settling behavior\n")
+ f.write(" โข Distinct collision responses\n\n")
+
+ f.write("MATERIAL PROPERTY RANGES:\n")
+ f.write("-" * 50 + "\n")
+ youngs_values = [mat["youngs_modulus_gpa"] for mat in cube_materials]
+ poisson_values = [mat["poisson_ratio"] for mat in cube_materials]
+ density_values = [mat["density"] for mat in cube_materials]
+
+ f.write(
+ f"Young's Modulus Range: {min(youngs_values):.6f} - {max(youngs_values):.6f} GPa\n"
+ )
+ f.write(
+ f"Poisson Ratio Range: {min(poisson_values):.6f} - {max(poisson_values):.6f}\n"
+ )
+ f.write(
+ f"Density Range: {min(density_values):.2f} - {max(density_values):.2f} kg/mยณ\n\n"
+ )
+
+ f.write("COLOR LEGEND:\n")
+ f.write("-" * 50 + "\n")
+ f.write(" ๐ข Green (Position 1): Original Material 1\n")
+ f.write(" ๐ต Dark Blue (Position 2): 25% Interpolated\n")
+ f.write(" ๐ต Medium Blue (Position 3): 50% Interpolated\n")
+ f.write(" ๐ต Light Blue (Position 4): 75% Interpolated\n")
+ f.write(" ๐ก Yellow (Position 5): Original Material 2\n")
+
+ return summary_file
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser(
+ description="5 cubes in a row with interpolated material properties"
+ )
+ parser.add_argument("--resolution", type=int, default=8)
+ parser.add_argument(
+ "--spacing", type=float, default=1.5, help="spacing between cube centers"
+ )
+ parser.add_argument(
+ "--drop_height", type=float, default=2.5, help="initial height of cubes"
+ )
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+
+ parser.add_argument(
+ "--pair",
+ type=int,
+ default=1,
+ help="Material pair to use (1 or 2 from JSON file)",
+ )
+
+ GroundCollidingSim.add_parser_arguments(parser)
+
+ parser.set_defaults(
+ n_frames=200,
+ gravity=9.81,
+ quasi_quasistatic=False,
+ young_modulus=1e4,
+ poisson_ratio=0.45,
+ density=1000.0,
+ ground=True,
+ ground_height=0.0,
+ collision_radius=0.05,
+ n_newton=100,
+ )
+
+ args = parser.parse_args()
+
+ material_data = load_material_interpolation_data()
+
+ pair_data = None
+ for pair in material_data:
+ if pair["pair_index"] == args.pair:
+ pair_data = pair
+ break
+
+ if pair_data is None:
+ available_pairs = [p["pair_index"] for p in material_data]
+ raise ValueError(
+ f"Material pair {args.pair} not found. Available pairs: {available_pairs}"
+ )
+
+ material_1 = pair_data["material_1"]
+ material_2 = pair_data["material_2"]
+ interpolated = pair_data["interpolated_materials"]
+
+ cube_materials = [
+ {
+ "name": f"{material_1['name']}",
+ "youngs_modulus_gpa": material_1["youngs_modulus_gpa"],
+ "poisson_ratio": material_1["poisson_ratio"],
+ "density": material_1["density"],
+ "is_fixed": False,
+ },
+ {
+ "name": f"Interpolated 0.25",
+ "youngs_modulus_gpa": interpolated[0]["target_properties"][
+ "youngs_modulus_gpa"
+ ],
+ "poisson_ratio": interpolated[0]["target_properties"]["poisson_ratio"],
+ "density": interpolated[0]["target_properties"]["density"],
+ "is_fixed": False,
+ },
+ {
+ "name": f"Interpolated 0.5",
+ "youngs_modulus_gpa": interpolated[1]["target_properties"][
+ "youngs_modulus_gpa"
+ ],
+ "poisson_ratio": interpolated[1]["target_properties"]["poisson_ratio"],
+ "density": interpolated[1]["target_properties"]["density"],
+ "is_fixed": False,
+ },
+ {
+ "name": f"Interpolated 0.75",
+ "youngs_modulus_gpa": interpolated[2]["target_properties"][
+ "youngs_modulus_gpa"
+ ],
+ "poisson_ratio": interpolated[2]["target_properties"]["poisson_ratio"],
+ "density": interpolated[2]["target_properties"]["density"],
+ "is_fixed": False,
+ },
+ {
+ "name": f"{material_2['name']}",
+ "youngs_modulus_gpa": material_2["youngs_modulus_gpa"],
+ "poisson_ratio": material_2["poisson_ratio"],
+ "density": material_2["density"],
+ "is_fixed": False,
+ },
+ ]
+
+ print(f"Using material pair {args.pair}:")
+ print(f"Material 1: {material_1['name']}")
+ print(f"Material 2: {material_2['name']}")
+ print(f"Simulation set to run for {args.n_frames} frames")
+ print(f"Creating 5 cubes in a row with material properties:")
+ print()
+
+ for i, mat in enumerate(cube_materials):
+ cube_type = "ORIGINAL" if i == 0 or i == 4 else "INTERPOLATED"
+ print(f"Cube {i+1}: {mat['name']} ({cube_type})")
+ print(f" Young's modulus: {mat['youngs_modulus_gpa']:.6f} GPa")
+ print(f" Poisson ratio: {mat['poisson_ratio']:.6f}")
+ print(f" Density: {mat['density']:.2f} kg/mยณ")
+ print()
+
+ sims = []
+
+ for i, mat in enumerate(cube_materials):
+ x_offset = i * args.spacing
+ geo = build_cube_geo(args.resolution, x_offset, height=args.drop_height)
+
+ local_args = copy.copy(args)
+ local_args.young_modulus = mat["youngs_modulus_gpa"] * 1e9
+ local_args.poisson_ratio = mat["poisson_ratio"]
+ local_args.density = mat["density"]
+
+ sim = GroundCollidingSim(geo, None, local_args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+ sim.init_collision_detector()
+
+ sim.set_boundary_condition(boundary_projector_form=None)
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ sims.append(sim)
+
+ n_frames = args.n_frames if hasattr(args, "n_frames") and args.n_frames > 0 else 200
+ recorded = [[] for _ in sims]
+
+ for i, sim in enumerate(sims):
+ recorded[i].append(
+ sim.u_field.space.node_positions().numpy() + sim.u_field.dof_values.numpy()
+ )
+
+ print(f"Running simulation for {n_frames} frames...")
+ for frame in range(n_frames):
+ for i, sim in enumerate(sims):
+ sim.run_frame()
+ recorded[i].append(
+ sim.u_field.space.node_positions().numpy()
+ + sim.u_field.dof_values.numpy()
+ )
+
+ if frame % 25 == 0:
+ print(f"Frame {frame+1}/{n_frames}")
+
+ if args.ui:
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ ps.init()
+ ps.set_window_size(1920, 1080)
+ ps.set_ground_plane_mode("shadow_only")
+ ps.set_ground_plane_height(0.0)
+
+ os.makedirs("cam", exist_ok=True)
+
+ cam_file = "cam/interp_cubes_fall.json"
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ meshes = []
+ for i, (sim, mat) in enumerate(zip(sims, cube_materials)):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ transparency = 0.7 if i == 0 or i == 4 else 0.6
+
+ m = ps.register_volume_mesh(
+ f"cube_{i}_{mat['name'].replace(' ', '_')}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=transparency,
+ )
+
+ if i == 0 or i == 4:
+ if i == 0:
+ m.set_color((0.2, 0.8, 0.2))
+ else:
+ m.set_color((0.8, 0.8, 0.2))
+ else:
+
+ interp_factor = (i - 1) / 2
+ blue_intensity = 0.3 + 0.5 * interp_factor
+ m.set_color((0.2, 0.2, blue_intensity))
+
+ meshes.append(m)
+
+ current = [0]
+ play = [False]
+ last = [time.time()]
+
+ def _ui():
+ changed, val = psim.SliderInt("frame", current[0], 0, n_frames - 1)
+ if changed:
+ current[0] = val
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[val])
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last[0] = time.time()
+
+ if psim.Button("Capture Camera View"):
+ view_json = ps.get_view_as_json()
+ filename = "cam/interp_cubes_fall.json"
+ with open(filename, "w") as f:
+ json.dump(json.loads(view_json), f, indent=2)
+ print(f"Camera view saved to {filename}")
+
+ if os.path.exists("cam/interp_cubes_fall.json"):
+ if psim.Button("Load Camera View"):
+ try:
+ with open("cam/interp_cubes_fall.json", "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print("Camera view loaded")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ psim.Text("Material Properties:")
+ psim.Text(
+ f"Pair: {args.pair} ({material_1['name']} โ {material_2['name']})"
+ )
+ psim.Text(f"Frames: {n_frames}")
+ psim.Text("Cube colors: Green = Original, Blue = Interpolated")
+ psim.Separator()
+
+ for i, mat in enumerate(cube_materials):
+ status = "ORIGINAL" if i == 0 or i == 4 else "INTERPOLATED"
+ psim.Text(f"Cube {i+1}: {status}")
+ psim.Text(f" YM: {mat['youngs_modulus_gpa']:.3f} GPa")
+ psim.Text(f" ฮฝ: {mat['poisson_ratio']:.3f}")
+ psim.Text(f" ฯ: {mat['density']:.0f} kg/mยณ")
+ if i < len(cube_materials) - 1:
+ psim.Separator()
+
+ if play[0] and time.time() - last[0] > 0.05:
+ current[0] = (current[0] + 1) % n_frames
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[current[0]])
+ last[0] = time.time()
+
+ ps.set_user_callback(_ui)
+ ps.show()
+
+ summary_dir = "outputs/interp_cubes_fall"
+ os.makedirs(summary_dir, exist_ok=True)
+ summary_file = write_material_properties_summary(
+ summary_dir, args, material_1, material_2, cube_materials
+ )
+ print(f"Material properties summary saved to: {summary_file}")
+ else:
+
+ import polyscope as ps
+ import subprocess
+ import glob
+
+ print(f"\nRunning headless mode for material pair {args.pair}")
+ print(f"Materials: {material_1['name']} โ {material_2['name']}")
+ print(f"Total frames: {n_frames}")
+
+ ps.init()
+ ps.set_ground_plane_mode("shadow_only")
+ ps.set_ground_plane_height(0.0)
+
+ output_dir = f"outputs/interp_cubes_fall/pair_{args.pair}"
+ frames_dir = f"{output_dir}/frames"
+ os.makedirs(frames_dir, exist_ok=True)
+
+ cam_file = "cam/interp_cubes_fall.json"
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ meshes = []
+ for i, (sim, mat) in enumerate(zip(sims, cube_materials)):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ transparency = 0.7 if i == 0 or i == 4 else 0.6
+ m = ps.register_volume_mesh(
+ f"cube_{i}_{mat['name'].replace(' ', '_')}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=transparency,
+ )
+
+ if i == 0 or i == 4:
+ if i == 0:
+ m.set_color((0.2, 0.8, 0.2))
+ else:
+ m.set_color((0.8, 0.8, 0.2))
+ else:
+
+ interp_factor = (i - 1) / 2
+ blue_intensity = 0.3 + 0.5 * interp_factor
+ m.set_color((0.2, 0.2, blue_intensity))
+
+ meshes.append(m)
+
+ screenshot_frames = [0, 8, 16, 20, 25, 199]
+ screenshot_frames = [f for f in screenshot_frames if f < n_frames]
+ print(
+ f"Saving {len(screenshot_frames)} screenshots at frames: {screenshot_frames}"
+ )
+
+ for i, frame_idx in enumerate(screenshot_frames):
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[frame_idx])
+
+ filename = f"{output_dir}/frame_{frame_idx:06d}.png"
+ ps.screenshot(filename)
+ print(f"Screenshot saved: {filename}")
+
+ print(f"Saving all {n_frames} frames for video creation...")
+
+ for frame_idx in range(n_frames):
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[frame_idx])
+
+ filename = f"{frames_dir}/frame_{frame_idx:06d}.png"
+ ps.screenshot(filename)
+
+ if frame_idx % 25 == 0:
+ print(f" Frame {frame_idx+1}/{n_frames}")
+
+ video_filename = f"{output_dir}/animation_pair_{args.pair}.mp4"
+ ffmpeg_cmd = [
+ "ffmpeg",
+ "-y",
+ "-framerate",
+ "20",
+ "-i",
+ f"{frames_dir}/frame_%06d.png",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-crf",
+ "18",
+ video_filename,
+ ]
+
+ try:
+ print(f"Creating video: {video_filename}")
+ subprocess.run(ffmpeg_cmd, check=True, capture_output=True)
+ print(f"Video created successfully: {video_filename}")
+
+ frame_files = glob.glob(f"{frames_dir}/*.png")
+ for frame_file in frame_files:
+ os.remove(frame_file)
+ os.rmdir(frames_dir)
+ print(f"Cleaned up {len(frame_files)} frame images")
+
+ except subprocess.CalledProcessError as e:
+ print(f"Error creating video with ffmpeg: {e}")
+ print(f"Frame images preserved in: {frames_dir}")
+ except FileNotFoundError:
+ print("ffmpeg not found. Install ffmpeg to create videos.")
+ print(f"Frame images saved in: {frames_dir}")
+
+ print(f"\nOutputs saved to {output_dir}/")
+ print(f"Material pair {args.pair}: {material_1['name']} โ {material_2['name']}")
+ print(f"Simulation completed: {n_frames} frames")
+ print("Screenshots saved:")
+ print(
+ f" - {len(screenshot_frames)} screenshots at frames: 0, 8, 16, 20, 25, 199"
+ )
+ print("Animation video created (if ffmpeg available)")
+ print("Color coding:")
+ print(" - Green cube (pos 1): Original material 1")
+ print(" - Blue cubes (pos 2-4): Interpolated materials")
+ print(" - Yellow cube (pos 5): Original material 2")
+ print("All cubes fall freely with their respective material properties")
+
+ summary_file = write_material_properties_summary(
+ output_dir, args, material_1, material_2, cube_materials
+ )
+ print(f"Material properties summary saved to: {summary_file}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/simulations/material_changes/material_pe_sensitivity.py b/deps/vomp/vomp/fem/simulations/material_changes/material_pe_sensitivity.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4d0fd544a6f42806748e8f9ac17b31556e481f8
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_changes/material_pe_sensitivity.py
@@ -0,0 +1,687 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import math
+import sys
+from pathlib import Path
+from typing import List, Tuple
+
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+
+from tqdm.auto import tqdm
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Field, Sample, Domain, integrand, normal
+
+
+from vomp.fem.simulations.compress_two_cubes import build_cube_geo, clamp_bottom_face
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM
+
+
+@fem.integrand
+def volume_integrand(s: Sample, domain: Domain):
+ """Integrand for computing volume."""
+ return 1.0
+
+
+def _compute_volume_change(sim: ClassicFEM) -> float:
+ initial_volume = 1.0
+
+ # Compute deformed volume using the Jacobian of the deformation
+ @fem.integrand
+ def deformed_volume_integrand(s: Sample, domain: Domain, u_cur: Field):
+ F = fem.grad(u_cur, s) + wp.identity(n=3, dtype=float)
+ J = wp.determinant(F)
+ return J
+
+ # Integrate over the domain to get the deformed volume
+ deformed_volume = fem.integrate(
+ deformed_volume_integrand,
+ fields={"u_cur": sim.u_field},
+ quadrature=sim.vel_quadrature,
+ output_dtype=float,
+ )
+
+ # Return relative volume change
+ return (float(deformed_volume) - initial_volume) / initial_volume
+
+
+def _simulate_cube(
+ young: float,
+ poisson: float,
+ density: float,
+ *,
+ resolution: int = 10,
+ force: float = 3e3,
+ n_frames: int = 30,
+) -> Tuple[float, float]:
+ """Runs a quasi-quasistatic vertical compression test on a *single* cube.
+
+ Parameters
+ ----------
+ young, poisson, density : material parameters
+ resolution : grid resolution along one axis
+ force : downward compressive force (N)
+ n_frames : solver frames (iterations)
+
+ Returns
+ -------
+ (Final potential energy, Volume change ratio)
+ """
+
+ geo = build_cube_geo(resolution, offset=(0.0, 0.0))
+
+ parser = argparse.ArgumentParser(add_help=False)
+ ClassicFEM.add_parser_arguments(parser)
+
+ args = parser.parse_args([])
+
+ args.young_modulus = young
+ args.poisson_ratio = poisson
+ args.density = density
+
+ args.quasi_quasistatic = True
+ args.gravity = 0.0
+ args.n_frames = n_frames
+
+ sim = ClassicFEM(geo, None, args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ sim.set_boundary_condition(boundary_projector_form=clamp_bottom_face)
+
+ top_center = wp.vec3(0.5, 1.0, 0.5)
+ sim.forces.count = 1
+ sim.forces.centers = wp.array([top_center], dtype=wp.vec3)
+ sim.forces.radii = wp.array([0.6], dtype=float)
+ sim.forces.forces = wp.array([wp.vec3(0.0, -force, 0.0)], dtype=wp.vec3)
+ sim.update_force_weight()
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ for _ in range(n_frames):
+ sim.run_frame()
+
+ E, _ = sim.evaluate_energy()
+ volume_change = _compute_volume_change(sim)
+ return float(E), volume_change
+
+
+def _simulate_cube_stretch(
+ young, poisson, density, *, resolution=10, force=3e3, n_frames=30
+):
+ """Pull right face of cube in +X with given force.
+
+ Returns
+ -------
+ (Final potential energy, Volume change ratio)
+ """
+ geo = build_cube_geo(resolution, offset=(0.0, 0.0))
+
+ parser_tmp = argparse.ArgumentParser(add_help=False)
+ ClassicFEM.add_parser_arguments(parser_tmp)
+ args_tmp = parser_tmp.parse_args([])
+ args_tmp.young_modulus = young
+ args_tmp.poisson_ratio = poisson
+ args_tmp.density = density
+ args_tmp.quasi_quasistatic = True
+ args_tmp.gravity = 0.0
+ args_tmp.n_frames = n_frames
+
+ sim = ClassicFEM(geo, None, args_tmp)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ @integrand
+ def clamp_left_face(s: Sample, domain: Domain, u: Field, v: Field):
+ nor = normal(domain, s)
+ clamped = wp.where(nor[0] < 0.0, 1.0, 0.0)
+ return wp.dot(u(s), v(s)) * clamped
+
+ sim.set_boundary_condition(boundary_projector_form=clamp_left_face)
+
+ right_center = wp.vec3(1.0, 0.5, 0.5)
+ sim.forces.count = 1
+ sim.forces.centers = wp.array([right_center], dtype=wp.vec3)
+ sim.forces.radii = wp.array([0.6], dtype=float)
+ sim.forces.forces = wp.array([wp.vec3(force, 0.0, 0.0)], dtype=wp.vec3)
+ sim.update_force_weight()
+
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ for _ in range(n_frames):
+ sim.run_frame()
+
+ E, _ = sim.evaluate_energy()
+ volume_change = _compute_volume_change(sim)
+ return float(E), volume_change
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser(
+ description="Material sensitivity test โ PE and volumetric displacement under compression vs. (ฯ, ฮฝ, E)",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ "--resolution", type=int, default=10, help="Cube grid resolution"
+ )
+ parser.add_argument(
+ "--force", type=float, default=3e3, help="Compressive force (N)"
+ )
+ parser.add_argument(
+ "--frames",
+ type=int,
+ default=100,
+ help="Quasi-static solver frames (per simulation)",
+ )
+ parser.add_argument(
+ "--rel_changes",
+ type=float,
+ nargs="*",
+ default=[0.01, 0.05, 0.10, 0.20, 0.25, 0.30],
+ help="Relative changes (proportions): 0.01 โ 1 %, 0.05 โ 5 %, etc.",
+ )
+ parser.add_argument(
+ "--triplets",
+ type=float,
+ nargs="*",
+ default=[],
+ help="Baseline material triplets listed as (E, ฮฝ, ฯ) โฆ",
+ )
+ parser.add_argument(
+ "--quick",
+ action=argparse.BooleanOptionalAction,
+ default=False,
+ help="Run a very small test grid (โ1โ2 triplets) to validate the setup",
+ )
+ parser.add_argument(
+ "--mode",
+ choices=["compress", "stretch", "both"],
+ default="compress",
+ help="Which loading scenario to run: compression, stretch, or both",
+ )
+ default_out = Path(__file__).resolve().parent
+ parser.add_argument(
+ "--out_dir", type=str, default=str(default_out), help="Where to save results"
+ )
+ args = parser.parse_args()
+
+ out_dir_path = Path(args.out_dir)
+ out_dir_path.mkdir(parents=True, exist_ok=True)
+ args.out_dir = str(out_dir_path)
+
+ if args.triplets:
+ if len(args.triplets) % 3 != 0:
+ sys.exit("--triplets must contain a multiple of 3 values (E, ฮฝ, ฯ โฆ)")
+ triplets: List[Tuple[float, float, float]] = [
+ tuple(args.triplets[i : i + 3]) for i in range(0, len(args.triplets), 3)
+ ]
+ else:
+
+ from itertools import product
+
+ if args.quick:
+
+ E_grid = [1e5, 1e6]
+ nu_grid = [0.30]
+ rho_grid = [1000.0]
+ else:
+
+ E_grid = [1e8, 1e6, 1e5]
+ nu_grid = [0.3, 0.35, 0.45]
+ rho_grid = [1000.0, 4000.0, 6000.0]
+
+ triplets = list(product(E_grid, nu_grid, rho_grid))
+
+ # Generate all material configurations to simulate
+ all_configs = []
+ for E0, nu0, rho0 in triplets:
+ # Add baseline
+ all_configs.append((E0, nu0, rho0))
+
+ # Add variations
+ for rel in args.rel_changes:
+ # Density variations
+ rho_new = rho0 * (1.0 + rel)
+ all_configs.append((E0, nu0, rho_new))
+
+ # Poisson ratio variations
+ nu_new = nu0 * (1.0 + rel)
+ if nu_new < 0.499:
+ all_configs.append((E0, nu_new, rho0))
+
+ # Young's modulus variations (using exponential scaling)
+ E_new = E0 * math.exp(rel)
+ all_configs.append((E_new, nu0, rho0))
+
+ # Remove duplicates
+ all_configs = list(set(all_configs))
+
+ total_est = len(all_configs)
+ print(f"Running {total_est} unique simulations...")
+
+ _pe_cache = {}
+ _vol_cache = {}
+
+ pbar = tqdm(total=total_est, desc="Simulations", unit="sim")
+
+ def cached_simulate(E, nu, rho, scenario):
+ key = (E, nu, rho, args.resolution, args.force, args.frames, scenario)
+ if key not in _pe_cache:
+ if scenario == "compress":
+ pe, vol_change = _simulate_cube(
+ E,
+ nu,
+ rho,
+ resolution=args.resolution,
+ force=args.force,
+ n_frames=args.frames,
+ )
+ else:
+ pe, vol_change = _simulate_cube_stretch(
+ E,
+ nu,
+ rho,
+ resolution=args.resolution,
+ force=args.force,
+ n_frames=args.frames,
+ )
+ _pe_cache[key] = pe
+ _vol_cache[key] = vol_change
+ pbar.update(1)
+ return _pe_cache[key], _vol_cache[key]
+
+ scenarios = [args.mode] if args.mode != "both" else ["compress", "stretch"]
+
+ for scenario in scenarios:
+
+ scenario_dir = out_dir_path / scenario
+ scenario_dir.mkdir(exist_ok=True)
+
+ # First, run all simulations and store results
+ sim_results = []
+ for E, nu, rho in all_configs:
+ PE, VOL = cached_simulate(E, nu, rho, scenario)
+ sim_results.append({"E": E, "nu": nu, "rho": rho, "PE": PE, "VOL": VOL})
+
+ # Save raw simulation results
+ raw_df = pd.DataFrame(sim_results)
+ # raw_df.to_csv(scenario_dir / "raw_simulation_results.csv", index=False)
+
+ # Now compute pairwise relative differences
+ records = []
+
+ for i, sim1 in enumerate(sim_results):
+ for j, sim2 in enumerate(sim_results):
+ if i >= j: # Skip self-comparisons and duplicates
+ continue
+
+ # Compute relative changes in parameters
+ rel_E = (sim2["E"] - sim1["E"]) / sim1["E"]
+ rel_nu = (
+ (sim2["nu"] - sim1["nu"]) / sim1["nu"] if sim1["nu"] != 0 else 0
+ )
+ rel_rho = (
+ (sim2["rho"] - sim1["rho"]) / sim1["rho"] if sim1["rho"] != 0 else 0
+ )
+
+ # Compute relative changes in outputs
+ rel_PE = (
+ (sim2["PE"] - sim1["PE"]) / sim1["PE"] if sim1["PE"] != 0 else 0
+ )
+ rel_VOL = (
+ (sim2["VOL"] - sim1["VOL"]) / sim1["VOL"] if sim1["VOL"] != 0 else 0
+ )
+
+ # Determine which parameter changed (if only one)
+ param_changed = None
+ param_rel_change = 0
+
+ # Check if only one parameter changed significantly (threshold 1e-6)
+ changes = []
+ if abs(rel_E) > 1e-6:
+ changes.append(("E", rel_E))
+ if abs(rel_nu) > 1e-6:
+ changes.append(("nu", rel_nu))
+ if abs(rel_rho) > 1e-6:
+ changes.append(("rho", rel_rho))
+
+ if len(changes) == 1:
+ param_changed, param_rel_change = changes[0]
+
+ records.append(
+ {
+ "sim1_idx": i,
+ "sim2_idx": j,
+ "E1": sim1["E"],
+ "nu1": sim1["nu"],
+ "rho1": sim1["rho"],
+ "E2": sim2["E"],
+ "nu2": sim2["nu"],
+ "rho2": sim2["rho"],
+ "rel_E": rel_E,
+ "rel_nu": rel_nu,
+ "rel_rho": rel_rho,
+ "param_changed": param_changed,
+ "param_rel_change": param_rel_change,
+ "rel_PE": rel_PE,
+ "rel_VOL": rel_VOL,
+ "abs_rel_PE": abs(rel_PE),
+ "abs_rel_VOL": abs(rel_VOL),
+ }
+ )
+
+ # Save all pairwise comparisons
+ df = pd.DataFrame(records)
+ # df.to_csv(scenario_dir / "pairwise_comparisons.csv", index=False)
+
+ # Create simplified summary statistics for specified relative changes only
+ summary_records = []
+ for param in ["E", "nu", "rho"]:
+ param_df = df[df["param_changed"] == param]
+
+ if len(param_df) == 0:
+ continue
+
+ # For each specified relative change
+ for rel_change in args.rel_changes:
+ # Find data points close to this relative change (within 1% tolerance)
+ tolerance = 0.01
+ matching_data = param_df[
+ (param_df["param_rel_change"] >= rel_change - tolerance)
+ & (param_df["param_rel_change"] <= rel_change + tolerance)
+ ]
+
+ if len(matching_data) > 0:
+ summary_records.append(
+ {
+ "parameter": param,
+ "relative_change": rel_change,
+ "PE_mean": matching_data["rel_PE"].mean(),
+ "PE_std": matching_data["rel_PE"].std(),
+ "VOL_mean": matching_data["rel_VOL"].mean(),
+ "VOL_std": matching_data["rel_VOL"].std(),
+ }
+ )
+
+ # Create DataFrame and save
+ summary_df = pd.DataFrame(summary_records)
+ summary_df.to_csv(scenario_dir / "sensitivity_summary.csv", index=False)
+
+ # For plotting, we still need the binned data
+ plot_summary_records = []
+ for param in ["E", "nu", "rho"]:
+ param_df = df[df["param_changed"] == param]
+
+ if len(param_df) == 0:
+ continue
+
+ # Group by approximate parameter change bins for plotting
+ bins = np.arange(-0.35, 0.36, 0.05)
+ param_df["param_bin"] = pd.cut(param_df["param_rel_change"], bins)
+
+ for bin_val in param_df["param_bin"].unique():
+ if pd.isna(bin_val):
+ continue
+
+ bin_data = param_df[param_df["param_bin"] == bin_val]
+ if len(bin_data) == 0:
+ continue
+
+ plot_summary_records.append(
+ {
+ "parameter": param,
+ "param_change_bin": str(bin_val),
+ "param_change_mean": bin_data["param_rel_change"].mean(),
+ "n_samples": len(bin_data),
+ "PE_rel_mean": bin_data["rel_PE"].mean(),
+ "PE_rel_std": bin_data["rel_PE"].std(),
+ "VOL_rel_mean": bin_data["rel_VOL"].mean(),
+ "VOL_rel_std": bin_data["rel_VOL"].std(),
+ }
+ )
+
+ plot_summary_df = pd.DataFrame(plot_summary_records)
+
+ # Create sensitivity plots from the plot summary data
+ for param in ["E", "nu", "rho"]:
+ param_summary = plot_summary_df[
+ plot_summary_df["parameter"] == param
+ ].copy()
+
+ if len(param_summary) == 0:
+ continue
+
+ # Sort by mean parameter change
+ param_summary = param_summary.sort_values("param_change_mean")
+
+ x = param_summary["param_change_mean"].values
+ y_pe = param_summary["PE_rel_mean"].values
+ y_pe_std = param_summary["PE_rel_std"].values
+ y_vol = param_summary["VOL_rel_mean"].values
+ y_vol_std = param_summary["VOL_rel_std"].values
+
+ # Configure matplotlib to use Helvetica font
+ plt.rcParams["font.family"] = "sans-serif"
+ plt.rcParams["font.sans-serif"] = ["Helvetica", "Arial", "DejaVu Sans"]
+ plt.rcParams["font.size"] = 5
+
+ # Plot PE sensitivity
+ fig, ax = plt.subplots(figsize=(2.1, 1.8))
+
+ # Remove title
+ # plt.title(f'PE vs ฮ{param}')
+
+ # Set axis labels with Delta notation
+ param_symbol = {"E": "E", "nu": "ฮฝ", "rho": "ฯ"}[param]
+ ax.set_xlabel(
+ f"$\\frac{{\\Delta {param_symbol}}}{{{param_symbol}}}$", fontsize=5
+ )
+
+ # Determine scale factor for y-axis
+ y_max_abs = max(
+ abs(y_pe.max()),
+ abs(y_pe.min()),
+ abs((y_pe + y_pe_std).max()),
+ abs((y_pe - y_pe_std).min()),
+ )
+ if y_max_abs == 0:
+ y_scale_factor = 1
+ y_scale_label = ""
+ elif y_max_abs < 0.01:
+ # Scale up to show in reasonable range
+ exponent = int(np.floor(np.log10(y_max_abs)))
+ y_scale_factor = 10 ** (-exponent)
+ y_scale_label = f"ร10$^{{{exponent}}}$"
+ elif y_max_abs > 10:
+ # Scale down
+ exponent = int(np.floor(np.log10(y_max_abs)))
+ y_scale_factor = 10 ** (-exponent)
+ y_scale_label = f"ร10$^{{{exponent}}}$"
+ else:
+ y_scale_factor = 1
+ y_scale_label = ""
+
+ # Scale the data
+ y_pe_scaled = y_pe * y_scale_factor
+ y_pe_std_scaled = y_pe_std * y_scale_factor
+
+ # Plot with confidence bands using light solid colors
+ (line,) = ax.plot(
+ x, y_pe_scaled, "-o", color="C0", markersize=4, linewidth=1.5
+ )
+ # Use a light blue color for the confidence band (no transparency)
+ ax.fill_between(
+ x,
+ y_pe_scaled - y_pe_std_scaled,
+ y_pe_scaled + y_pe_std_scaled,
+ color="#CCE5FF", # Light blue, solid color
+ edgecolor="none",
+ zorder=0,
+ ) # Put behind the line
+
+ # Remove grid and dashed lines at 0
+ ax.grid(False)
+
+ # Remove top and right spines
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+
+ # Make remaining borders thicker
+ ax.spines["left"].set_linewidth(1.5)
+ ax.spines["bottom"].set_linewidth(1.5)
+
+ # Set y-axis label horizontally at the top, starting from the y-axis position
+ if y_scale_label:
+ y_label_text = f"$\\frac{{\\Delta PE}}{{PE}}$ ({y_scale_label})"
+ else:
+ y_label_text = "$\\frac{\\Delta PE}{PE}$"
+ ax.text(
+ 0,
+ 1.02,
+ y_label_text,
+ transform=ax.transAxes,
+ ha="left",
+ va="bottom",
+ fontsize=5,
+ )
+
+ # Add some padding between data and borders
+ ax.margins(x=0.05, y=0.1)
+
+ # Set tick label font size
+ ax.tick_params(axis="both", which="major", labelsize=5)
+
+ plt.tight_layout(pad=0)
+ plt.savefig(
+ scenario_dir / f"sensitivity_{param}_PE.pdf",
+ dpi=300,
+ bbox_inches="tight",
+ pad_inches=0,
+ )
+ plt.close()
+
+ # Plot Volume sensitivity
+ fig, ax = plt.subplots(figsize=(2.1, 1.8))
+
+ # Remove title
+ # plt.title(f'Volume Change vs ฮ{param}')
+
+ # Set axis labels with Delta notation
+ ax.set_xlabel(
+ f"$\\frac{{\\Delta {param_symbol}}}{{{param_symbol}}}$", fontsize=5
+ )
+
+ # Determine scale factor for y-axis
+ y_max_abs = max(
+ abs(y_vol.max()),
+ abs(y_vol.min()),
+ abs((y_vol + y_vol_std).max()),
+ abs((y_vol - y_vol_std).min()),
+ )
+ if y_max_abs == 0:
+ y_scale_factor = 1
+ y_scale_label = ""
+ elif y_max_abs < 0.01:
+ # Scale up to show in reasonable range
+ exponent = int(np.floor(np.log10(y_max_abs)))
+ y_scale_factor = 10 ** (-exponent)
+ y_scale_label = f"ร10$^{{{exponent}}}$"
+ elif y_max_abs > 10:
+ # Scale down
+ exponent = int(np.floor(np.log10(y_max_abs)))
+ y_scale_factor = 10 ** (-exponent)
+ y_scale_label = f"ร10$^{{{exponent}}}$"
+ else:
+ y_scale_factor = 1
+ y_scale_label = ""
+
+ # Scale the data
+ y_vol_scaled = y_vol * y_scale_factor
+ y_vol_std_scaled = y_vol_std * y_scale_factor
+
+ # Plot with confidence bands using light solid colors
+ (line,) = ax.plot(
+ x, y_vol_scaled, "-o", color="C1", markersize=4, linewidth=1.5
+ )
+ # Use a light orange color for the confidence band (no transparency)
+ ax.fill_between(
+ x,
+ y_vol_scaled - y_vol_std_scaled,
+ y_vol_scaled + y_vol_std_scaled,
+ color="#FFE5CC", # Light orange, solid color
+ edgecolor="none",
+ zorder=0,
+ ) # Put behind the line
+
+ # Remove grid and dashed lines at 0
+ ax.grid(False)
+
+ # Remove top and right spines
+ ax.spines["top"].set_visible(False)
+ ax.spines["right"].set_visible(False)
+
+ # Make remaining borders thicker
+ ax.spines["left"].set_linewidth(1.5)
+ ax.spines["bottom"].set_linewidth(1.5)
+
+ # Set y-axis label horizontally at the top, starting from the y-axis position
+ if y_scale_label:
+ y_label_text = f"$\\frac{{\\Delta V}}{{V}}$ ({y_scale_label})"
+ else:
+ y_label_text = "$\\frac{\\Delta V}{V}$"
+ ax.text(
+ 0,
+ 1.02,
+ y_label_text,
+ transform=ax.transAxes,
+ ha="left",
+ va="bottom",
+ fontsize=5,
+ )
+
+ # Add some padding between data and borders
+ ax.margins(x=0.05, y=0.1)
+
+ # Set tick label font size
+ ax.tick_params(axis="both", which="major", labelsize=5)
+
+ plt.tight_layout(pad=0)
+ plt.savefig(
+ scenario_dir / f"sensitivity_{param}_VOL.pdf",
+ dpi=300,
+ bbox_inches="tight",
+ pad_inches=0,
+ )
+ plt.close()
+
+ pbar.close()
+
+ print("\nDone.")
+ print(f"Results saved to: {args.out_dir}")
+ print(f"Total unique simulations run: {len(all_configs)}")
+ print(f"Total pairwise comparisons: {len(records)}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/simulations/material_changes/results/cable_tension_200N/stretch/sensitivity_summary.csv b/deps/vomp/vomp/fem/simulations/material_changes/results/cable_tension_200N/stretch/sensitivity_summary.csv
new file mode 100644
index 0000000000000000000000000000000000000000..cdde78f58c58afa50a34a9c7c112fd15269a3774
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_changes/results/cable_tension_200N/stretch/sensitivity_summary.csv
@@ -0,0 +1,17 @@
+parameter,relative_change,PE_mean,PE_std,VOL_mean,VOL_std
+E,0.01,-0.0129038085515901,0.013041219038378364,-0.015035706563852306,0.017834125233349717
+E,0.05,-0.04094336955976991,0.011889046744531178,-0.04576756359214442,0.008493404407395332
+E,0.1,-0.08606801986507683,0.020873323658437,-0.09179976736843398,0.007970744164979622
+E,0.2,-0.15296883373857365,0.0463778886139687,-0.1660379750718916,0.017187661655442603
+nu,0.01,-0.00041418672715554935,0.011648404184717983,-0.0395249327079461,0.02916278003866443
+nu,0.05,0.04623747659328452,0.2991550800560806,-0.18801314979895714,0.17236141308517147
+nu,0.1,-0.0155555392330182,0.05066027043342086,-0.24383180560520537,0.16669793624856405
+nu,0.2,0.003327941221950312,0.06425568536106863,-0.43373446822435857,0.10397373578694094
+nu,0.25,0.002902289348248634,0.06551265250292741,-0.4431740164779652,0.12001269701066668
+nu,0.3,0.03864829261054049,0.11933797608550176,-0.6200795405365392,0.06215817458406742
+rho,0.01,0.00284176850786049,0.008351915641024093,-0.001555567235735924,0.00815507732028957
+rho,0.05,-0.0018551366368292162,0.012169160948021097,0.0010493177123636981,0.010256456710272836
+rho,0.1,0.004417289038605687,0.015571798419707392,-0.0005632567464132381,0.00835109956066956
+rho,0.2,0.0012798429360461646,0.01874221916147674,0.0032735253311144265,0.009381932117685858
+rho,0.25,-0.002829245996259952,0.013759290444916613,-0.0009670969387826114,0.008231047552147175
+rho,0.3,-0.0016526366215512387,0.0065630864764682955,-0.0023036717730552008,0.007567563924109837
diff --git a/deps/vomp/vomp/fem/simulations/material_changes/results/package_drop_120N/compress/sensitivity_summary.csv b/deps/vomp/vomp/fem/simulations/material_changes/results/package_drop_120N/compress/sensitivity_summary.csv
new file mode 100644
index 0000000000000000000000000000000000000000..9f59de1a79adc105b4283c3da93e81efae0cdb19
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_changes/results/package_drop_120N/compress/sensitivity_summary.csv
@@ -0,0 +1,17 @@
+parameter,relative_change,PE_mean,PE_std,VOL_mean,VOL_std
+E,0.01,0.015303343134713635,0.054559433014160275,1.021988275042958,2.9700735579712187
+E,0.05,-0.03592161643585864,0.03580635757703928,-0.05627754382133505,0.33741996788187645
+E,0.1,-0.07963777355186127,0.036377850846202375,-0.13970485367443125,0.18271921022643806
+E,0.2,-0.14176392969065477,0.07303815125488342,-0.3155211945270783,0.40336920896380823
+nu,0.01,0.013111397204394292,0.04986563923227492,-0.28353115201304446,0.5540887313156924
+nu,0.05,0.11871577848744365,0.6267711737859302,-0.25472449884135046,0.6401832634917356
+nu,0.1,-0.0011471017632540888,0.07218815040002759,-0.2879195458055409,0.16993008466858853
+nu,0.2,0.049943769966931804,0.15768182858426202,-0.5381890777532831,0.18031323857156625
+nu,0.25,0.05529270032094147,0.17818312467871422,-0.5484646269212342,0.19996529884406822
+nu,0.3,0.21529728199558784,0.32859089623486865,-0.8323952103626067,0.18112745701165572
+rho,0.01,-0.0015363926045339447,0.03804325146957454,-0.0193332149053109,0.07143316159667659
+rho,0.05,-0.0006471219440979866,0.026577566508606102,0.1342872475835216,1.6590457338635072
+rho,0.1,0.008671590031983303,0.03366022207417588,0.02914893109440586,0.22977177796023915
+rho,0.2,-0.002729465535162169,0.020843618006083968,0.8998720190536853,2.971970014572777
+rho,0.25,0.0008017437643783554,0.022992265757040914,0.49997293861031017,2.2121509540597457
+rho,0.3,-0.003977153662133068,0.03631157825549312,-0.011964089069548088,0.07361021837783799
diff --git a/deps/vomp/vomp/fem/simulations/material_changes/results/robot_gripper_140N/compress/sensitivity_summary.csv b/deps/vomp/vomp/fem/simulations/material_changes/results/robot_gripper_140N/compress/sensitivity_summary.csv
new file mode 100644
index 0000000000000000000000000000000000000000..d2a6d4cc0ef354808aa64e547033b0ca7aa47a00
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_changes/results/robot_gripper_140N/compress/sensitivity_summary.csv
@@ -0,0 +1,17 @@
+parameter,relative_change,PE_mean,PE_std,VOL_mean,VOL_std
+E,0.01,-0.005111979137049018,0.013551671510642755,0.019873285363137087,0.13640276167167453
+E,0.05,-0.0323755227376545,0.032870560039154374,-0.04156828339513993,0.10838409688130228
+E,0.1,-0.07007548156680934,0.055015403137479046,-0.10021986326661587,0.03643470317378153
+E,0.2,-0.13441952932925932,0.09427950988604199,-0.266060975339183,0.2512039754489314
+nu,0.01,-0.0036469233416082203,0.0282104052157145,0.03722949619901666,0.43825133060950794
+nu,0.05,0.09779596646163438,0.5352956730076497,-0.1998799899979623,0.47885449942454905
+nu,0.1,-0.005694012772650905,0.061045671820691906,-0.2821469534899,0.17246927219773836
+nu,0.2,0.04334694004202131,0.1372766029321473,-0.5297982931070182,0.15847461434102317
+nu,0.25,0.04441946579653962,0.14312792563836527,-0.5324872921887855,0.1726956694161042
+nu,0.3,0.14129007449057004,0.23892668936694264,-0.7969507537713615,0.14187313607903668
+rho,0.01,0.00267623626901207,0.013426021755418202,-0.015245291026764396,0.08725808639022989
+rho,0.05,0.002654402513971362,0.018670984871934346,0.006756292684796867,0.15774850986730302
+rho,0.1,0.0036321582925728027,0.02028651734027726,-0.007968982795043917,0.03040028348762187
+rho,0.2,-0.0020882727916316994,0.021196507362892983,-0.018257019698772355,0.11795135868741784
+rho,0.25,-0.002681918998921467,0.02694863890211054,-0.011749521387510469,0.12825466324057647
+rho,0.3,0.005014763943560293,0.019054775026389685,-0.03100767661379415,0.14017639415725092
diff --git a/deps/vomp/vomp/fem/simulations/material_changes/results/tensile_test_330N/stretch/sensitivity_summary.csv b/deps/vomp/vomp/fem/simulations/material_changes/results/tensile_test_330N/stretch/sensitivity_summary.csv
new file mode 100644
index 0000000000000000000000000000000000000000..cf59439e5d61a91f16690de36943be24407a69f2
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_changes/results/tensile_test_330N/stretch/sensitivity_summary.csv
@@ -0,0 +1,17 @@
+parameter,relative_change,PE_mean,PE_std,VOL_mean,VOL_std
+E,0.01,-0.004660373208836219,0.006526677198457342,-0.007704182037023084,0.008815786146080971
+E,0.05,-0.04376663571209795,0.0072262966121624285,-0.044097930247297294,0.006988115345506259
+E,0.1,-0.09034074468926426,0.009477682511280653,-0.09070934369816719,0.008605478794579437
+E,0.2,-0.16559980006437547,0.01711295057699323,-0.16511037075557966,0.016468513163860533
+nu,0.01,-0.001335652648966959,0.006459792163862618,-0.039190717913903876,0.026540540249085764
+nu,0.05,0.012410568332277973,0.15005851577143547,-0.19242477479696352,0.17493455441149064
+nu,0.1,-0.02164331577770946,0.0465264229562904,-0.24711715183419095,0.1661141326799227
+nu,0.2,-0.023878141536717367,0.022405648286458434,-0.44202822680359743,0.10048949694410814
+nu,0.25,-0.02438164918910948,0.023636379616944744,-0.452492075750608,0.11711466467844865
+nu,0.3,-0.024375353923958863,0.04696446205401704,-0.6327097745373522,0.048615604793616034
+rho,0.01,0.0013575535077880079,0.004153969263369369,-0.0009224751723457108,0.00399592131524107
+rho,0.05,0.0014126874232578115,0.004900159244007686,0.00016065969035801314,0.0052643877898904306
+rho,0.1,0.0006082471443082579,0.004217447408912523,-9.828818728756298e-06,0.0020819292641315826
+rho,0.2,-0.0004945932671975583,0.0037524942667321987,-0.00017503419648881355,0.008407609612391606
+rho,0.25,0.0020401504578021713,0.00737919210022767,0.0015096727111267077,0.009339124088676078
+rho,0.3,0.0009317479276056047,0.004617407313070384,-0.00020479253671939487,0.003923567249139898
diff --git a/deps/vomp/vomp/fem/simulations/material_changes/run_all_scenarios.sh b/deps/vomp/vomp/fem/simulations/material_changes/run_all_scenarios.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c8adf7121b071bd807b143ee8b2d3d3529d67f2d
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_changes/run_all_scenarios.sh
@@ -0,0 +1,31 @@
+echo "Creating results directory structure..."
+mkdir -p results/{robot_gripper_140N,package_drop_120N}
+mkdir -p results/{tensile_test_330N,cable_tension_200N}
+
+echo "Running Scenario 1: Robot Gripper Compression (140 N)..."
+python material_pe_sensitivity.py \
+ --mode compress \
+ --force 140 \
+ --out_dir results/robot_gripper_140N \
+ --frames 50
+
+echo "Running Scenario 2: Package Drop Test - 2 ft (120 N)..."
+python material_pe_sensitivity.py \
+ --mode compress \
+ --force 120 \
+ --out_dir results/package_drop_120N \
+ --frames 50
+
+echo "Running Scenario 3: Tensile Testing Machine (500 N)..."
+python material_pe_sensitivity.py \
+ --mode stretch \
+ --force 330 \
+ --out_dir results/tensile_test_330N \
+ --frames 50
+
+echo "Running Scenario 4: Cable/Wire Tension (200 N)..."
+python material_pe_sensitivity.py \
+ --mode stretch \
+ --force 200 \
+ --out_dir results/cable_tension_200N \
+ --frames 50
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/material_interpolation_results.json b/deps/vomp/vomp/fem/simulations/material_interpolation_results.json
new file mode 100644
index 0000000000000000000000000000000000000000..18dcebf179805381a2b94943c17b8682b7c6e326
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/material_interpolation_results.json
@@ -0,0 +1,120 @@
+[
+ {
+ "pair_index": 1,
+ "material_1": {
+ "name": "Balsa Wood",
+ "youngs_modulus_gpa": 3.10187149402563,
+ "poisson_ratio": 0.2201955512,
+ "density": 175.9424556614
+ },
+ "material_2": {
+ "name": "Chloroprene Rubber (Neoprene)",
+ "youngs_modulus_gpa": 0.005,
+ "poisson_ratio": 0.49,
+ "density": 1200.0
+ },
+ "interpolated_materials": [
+ {
+ "interpolation_factor": 0.25,
+ "target_properties": {
+ "youngs_modulus_gpa": 2.3276536205192224,
+ "poisson_ratio": 0.2876466634,
+ "density": 431.95684174605003
+ },
+ "closest_real_material": {
+ "name": "Wood (Perpendicular to Grain)",
+ "youngs_modulus_gpa": 1.2206895568453686,
+ "poisson_ratio": 0.3002203342,
+ "density": 415.4173359812
+ }
+ },
+ {
+ "interpolation_factor": 0.5,
+ "target_properties": {
+ "youngs_modulus_gpa": 1.553435747012815,
+ "poisson_ratio": 0.3550977756,
+ "density": 687.9712278307
+ },
+ "closest_real_material": {
+ "name": "Wood (Perpendicular to Grain)",
+ "youngs_modulus_gpa": 1.9766258584516208,
+ "poisson_ratio": 0.3546132972,
+ "density": 696.2623876324
+ }
+ },
+ {
+ "interpolation_factor": 0.75,
+ "target_properties": {
+ "youngs_modulus_gpa": 0.7792178735064075,
+ "poisson_ratio": 0.4225488878,
+ "density": 943.98561391535
+ },
+ "closest_real_material": {
+ "name": "Polybutylene (PB)",
+ "youngs_modulus_gpa": 0.28866436439665166,
+ "poisson_ratio": 0.4196274152,
+ "density": 942.7536376605
+ }
+ }
+ ]
+ },
+ {
+ "pair_index": 2,
+ "material_1": {
+ "name": "Epoxy Resin",
+ "youngs_modulus_gpa": 2.41,
+ "poisson_ratio": 0.35,
+ "density": 1200.0
+ },
+ "material_2": {
+ "name": "EPDM Rubber",
+ "youngs_modulus_gpa": 0.01,
+ "poisson_ratio": 0.49,
+ "density": 1100.0
+ },
+ "interpolated_materials": [
+ {
+ "interpolation_factor": 0.25,
+ "target_properties": {
+ "youngs_modulus_gpa": 1.81,
+ "poisson_ratio": 0.38499999999999995,
+ "density": 1175.0
+ },
+ "closest_real_material": {
+ "name": "Plastics",
+ "youngs_modulus_gpa": 1.7284591916589245,
+ "poisson_ratio": 0.3843803289,
+ "density": 1175.0
+ }
+ },
+ {
+ "interpolation_factor": 0.5,
+ "target_properties": {
+ "youngs_modulus_gpa": 1.21,
+ "poisson_ratio": 0.42,
+ "density": 1150.0
+ },
+ "closest_real_material": {
+ "name": "Nylon",
+ "youngs_modulus_gpa": 2.565507357759067,
+ "poisson_ratio": 0.4197987872,
+ "density": 1145.5001730965
+ }
+ },
+ {
+ "interpolation_factor": 0.75,
+ "target_properties": {
+ "youngs_modulus_gpa": 0.61,
+ "poisson_ratio": 0.45499999999999996,
+ "density": 1125.0
+ },
+ "closest_real_material": {
+ "name": "Liquid Oxygen",
+ "youngs_modulus_gpa": 0.005244663533763157,
+ "poisson_ratio": 0.4552196218,
+ "density": 1141.0
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/deps/vomp/vomp/fem/simulations/stretch_two_cubes.py b/deps/vomp/vomp/fem/simulations/stretch_two_cubes.py
new file mode 100644
index 0000000000000000000000000000000000000000..2dec70232a88cd0fcf7ff2064b75e0655b404c7a
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/stretch_two_cubes.py
@@ -0,0 +1,436 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+import json
+import os
+
+import warp as wp
+import warp.fem as fem
+from warp.fem import Domain, Sample, Field, integrand, normal
+
+import sys
+import copy
+
+# Add the parent directory to the Python path
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM, run_softbody_sim
+from vomp.fem.fem_examples.mfem.demo_3d import clamped_sides
+
+# -----------------------------------------------------------------------------
+# Helper: build cube geometry at given (x, z) offset and optional height (y)
+# -----------------------------------------------------------------------------
+
+
+def build_cube_geo(res: int, offset, height: float = 0.0):
+ """Return a Grid3D geometry for one cube centred at offset (x,z)."""
+ lo = wp.vec3(offset[0], height, offset[1])
+ hi = wp.vec3(offset[0] + 1.0, height + 1.0, offset[1] + 1.0)
+ return fem.Grid3D(res=wp.vec3i(res), bounds_lo=lo, bounds_hi=hi)
+
+
+# -----------------------------------------------------------------------------
+# Integrand utilities for boundary conditions
+# -----------------------------------------------------------------------------
+
+
+@integrand
+def clamp_left_face(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Clamp nodes on the left face (normal.x < 0)."""
+ nor = normal(domain, s)
+ clamped = wp.where(nor[0] < 0.0, 1.0, 0.0)
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def clamp_right_face(
+ s: Sample,
+ domain: Domain,
+ u: Field,
+ v: Field,
+):
+ """Clamp nodes on right face (normal.x > 0)."""
+ nor = normal(domain, s)
+ clamped = wp.where(nor[0] > 0.0, 1.0, 0.0)
+ return wp.dot(u(s), v(s)) * clamped
+
+
+@integrand
+def right_face_displacement_form(
+ s: Sample,
+ domain: Domain,
+ v: Field,
+ displacement: float = 0.0,
+):
+ """Dirichlet RHS contribution: displacement * test_x on the right face."""
+ nor = normal(domain, s)
+ on_face = wp.where(nor[0] > 0.0, 1.0, 0.0)
+ return -displacement * wp.dot(wp.vec3(1.0, 0.0, 0.0), v(s)) * on_face
+
+
+# Per-node value version used by set_fixed_points_displacement (no test function)
+@integrand
+def right_face_displacement_field(
+ s: Sample,
+ domain: Domain,
+ u_cur: Field = None,
+ displacement: float = 0.0,
+):
+ nor = normal(domain, s)
+ on_face = wp.where(nor[0] > 0.0, 1.0, 0.0)
+ return wp.vec3(displacement * on_face, 0.0, 0.0)
+
+
+# -----------------------------------------------------------------------------
+# Main
+# -----------------------------------------------------------------------------
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--resolution", type=int, default=8)
+ parser.add_argument(
+ "--spacing", type=float, default=1.5, help="center spacing between cubes"
+ )
+ parser.add_argument(
+ "--stretch",
+ type=float,
+ default=0.3,
+ help="Total +x displacement applied to the right face of the second cube",
+ )
+ parser.add_argument("--ui", action=argparse.BooleanOptionalAction, default=True)
+ parser.add_argument(
+ "--force",
+ type=float,
+ default=2e3,
+ help="Magnitude of the pulling force (+X direction) applied to the right cube (in Newtons)",
+ )
+ parser.add_argument(
+ "--ramp",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Linearly ramp the external force over the frames instead of applying it instantly",
+ )
+
+ # Per-cube material parameters (two values) default to same as single default
+ parser.add_argument(
+ "--youngs",
+ type=float,
+ nargs=2,
+ default=[1e4, 3e4],
+ help="Young's modulus for cube0 cube1",
+ )
+ parser.add_argument(
+ "--poissons",
+ type=float,
+ nargs=2,
+ default=[0.45, 0.45],
+ help="Poisson ratio for cube0 cube1",
+ )
+ parser.add_argument(
+ "--densities",
+ type=float,
+ nargs=2,
+ default=[1000.0, 1000.0],
+ help="Density for cube0 cube1",
+ )
+
+ # Add generic FEM simulator arguments
+ ClassicFEM.add_parser_arguments(parser)
+
+ # Provide some defaults more suitable for a quasistatic stretch test
+ parser.set_defaults(
+ n_frames=100,
+ quasi_quasistatic=True,
+ gravity=0.0, # disable gravity so we only observe stretching
+ n_newton=8,
+ young_modulus=1e4,
+ poisson_ratio=0.45,
+ density=1000.0,
+ )
+
+ args = parser.parse_args()
+
+ # Build two cube geometries side by side along x
+ offsets = [(0.0, 0.0), (args.spacing, 0.0)]
+ sims = []
+ force_sims = [] # store tuples (sim, sign) where sign=+1 for +x, -1 for -x
+
+ for idx, offs in enumerate(offsets):
+ geo = build_cube_geo(args.resolution, offs, height=0.0)
+
+ # make a shallow copy of args and override material parameters for this cube
+ local_args = copy.copy(args)
+ local_args.young_modulus = args.youngs[idx]
+ local_args.poisson_ratio = args.poissons[idx]
+ local_args.density = args.densities[idx]
+
+ sim = ClassicFEM(geo, None, local_args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+
+ x_min = offs[0]
+ x_max = offs[0] + 1.0
+
+ if idx == 0:
+ # Left cube: clamp its right (inner) face; pull on left face (-X)
+ sim.set_boundary_condition(boundary_projector_form=clamp_right_face)
+ center = wp.vec3(x_min, 0.5, 0.5) # outer left face centre
+ direction = -1.0
+ else:
+ # Right cube: clamp its left (inner) face; pull on right face (+X)
+ sim.set_boundary_condition(boundary_projector_form=clamp_left_face)
+ center = wp.vec3(x_max, 0.5, 0.5)
+ direction = 1.0
+
+ # create volumetric pulling force
+ sim.forces.count = 1
+ sim.forces.centers = wp.array([center], dtype=wp.vec3)
+ sim.forces.radii = wp.array([0.6], dtype=float)
+ sim.forces.forces = wp.array(
+ [wp.vec3(direction * args.force, 0.0, 0.0)], dtype=wp.vec3
+ )
+ sim.update_force_weight()
+
+ force_sims.append((sim, direction))
+
+ # Allocate matrices for Newton solver
+ sim.init_constant_forms()
+ sim.project_constant_forms()
+
+ sims.append(sim)
+
+ # Record frames if UI requested
+ recorded = [[] for _ in sims]
+ n_frames = args.n_frames if args.n_frames > 0 else 1
+
+ def make_recorder(i):
+ def _rec(pos):
+ recorded[i].append(pos.copy())
+
+ return _rec
+
+ for i, sim in enumerate(sims):
+ extra0 = sim.v_bd_rhs.numpy() if sim.v_bd_rhs is not None else 0.0
+ recorded[i].append(sim.u_field.space.node_positions().numpy() + extra0)
+
+ # Run simulation frames
+ for frame in range(n_frames):
+ # Update external force magnitude if ramping
+ if args.ramp:
+ scale = float(frame + 1) / float(n_frames)
+ for sim, direction in force_sims:
+ sim.forces.forces = wp.array(
+ [wp.vec3(direction * args.force * scale, 0.0, 0.0)], dtype=wp.vec3
+ )
+ sim.update_force_weight()
+
+ for i, sim in enumerate(sims):
+ sim.run_frame()
+ extra = sim.v_bd_rhs.numpy() if sim.v_bd_rhs is not None else 0.0
+ pos = (
+ sim.u_field.space.node_positions().numpy()
+ + sim.u_field.dof_values.numpy()
+ + extra
+ )
+ recorded[i].append(pos)
+
+ # Print energies for quick feedback
+ en_frame = [float(sim.evaluate_energy()[0]) for sim in sims]
+ print(f"Frame {frame+1}/{n_frames} energies: {en_frame}")
+
+ # Compute potential energy for both cubes after final frame
+ energies = []
+ for sim in sims:
+ E, _ = sim.evaluate_energy()
+ energies.append(float(E))
+
+ print("Potential energies (left_cube, right_cube):", energies)
+
+ # Visualize using Polyscope if requested
+ if args.ui:
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ ps.init()
+ ps.set_ground_plane_mode("shadow_only")
+
+ # Create cam directory if it doesn't exist
+ os.makedirs("cam", exist_ok=True)
+
+ # Load camera view if it exists
+ cam_file = "cam/stretch_two_cubes.json"
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ meshes = []
+ for i, sim in enumerate(sims):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ m = ps.register_volume_mesh(
+ f"cube_{i}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=0.6,
+ )
+ meshes.append(m)
+
+ current = [0]
+ play = [False]
+ last = [time.time()]
+
+ def _ui():
+ changed, val = psim.SliderInt("frame", current[0], 0, n_frames)
+ if changed:
+ current[0] = val
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[val])
+
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last[0] = time.time()
+
+ # Camera capture button
+ if psim.Button("Capture Camera View"):
+ # Get current camera view as JSON
+ view_json = ps.get_view_as_json()
+ # Simple filename
+ filename = "cam/stretch_two_cubes.json"
+ # Save to file
+ with open(filename, "w") as f:
+ json.dump(json.loads(view_json), f, indent=2)
+ print(f"Camera view saved to {filename}")
+
+ # Load camera view dropdown
+ if os.path.exists("cam"):
+ cam_files = [f for f in os.listdir("cam") if f.endswith(".json")]
+ if cam_files:
+ psim.Text("Load Camera View:")
+ for cam_file in sorted(cam_files):
+ if psim.Button(f"Load {cam_file}"):
+ try:
+ with open(f"cam/{cam_file}", "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ if play[0] and time.time() - last[0] > 0.05:
+ current[0] = (current[0] + 1) % (n_frames + 1)
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[current[0]])
+ last[0] = time.time()
+
+ ps.set_user_callback(_ui)
+ ps.show()
+ else:
+ # Headless mode - save screenshots
+ import polyscope as ps
+
+ ps.init()
+ ps.set_ground_plane_mode("shadow_only")
+
+ # Create output directory
+ output_dir = "outputs/stretch_two_cubes"
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Load camera view if it exists
+ cam_file = "cam/stretch_two_cubes.json"
+ if os.path.exists(cam_file):
+ try:
+ with open(cam_file, "r") as f:
+ view_data = json.load(f)
+ view_json = json.dumps(view_data)
+ ps.set_view_from_json(view_json)
+ print(f"Loaded camera view from {cam_file}")
+ except Exception as e:
+ print(f"Error loading camera view: {e}")
+
+ # Register meshes
+ meshes = []
+ for i, sim in enumerate(sims):
+ try:
+ hexes = sim.u_field.space.node_hexes()
+ except AttributeError:
+ hexes = None
+
+ if hexes is None:
+ try:
+ tets = sim.u_field.space.node_tets()
+ except AttributeError:
+ tets = None
+ else:
+ tets = None
+
+ m = ps.register_volume_mesh(
+ f"cube_{i}",
+ recorded[i][0],
+ hexes=hexes,
+ tets=tets,
+ edge_width=0.0,
+ transparency=0.6,
+ )
+ meshes.append(m)
+
+ # Save 10 evenly distributed frames
+ screenshot_frames = [int(i * n_frames / 9) for i in range(10)]
+ print(f"Saving screenshots for frames: {screenshot_frames}")
+
+ for frame_idx in screenshot_frames:
+ # Update mesh positions for this frame
+ for m, rec in zip(meshes, recorded):
+ m.update_vertex_positions(rec[frame_idx])
+
+ # Take screenshot
+ filename = f"{output_dir}/frame_{frame_idx:06d}.png"
+ ps.screenshot(filename)
+ print(f"Screenshot saved: {filename}")
+
+ print(f"All screenshots saved to {output_dir}/")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/fem/simulations/usd_drop.py b/deps/vomp/vomp/fem/simulations/usd_drop.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4ddd1c2401e6fd920a54c89c08a390d4b057eb2
--- /dev/null
+++ b/deps/vomp/vomp/fem/simulations/usd_drop.py
@@ -0,0 +1,599 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import time
+import numpy as np
+import os
+import sys
+
+import warp as wp
+import warp.fem as fem
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+from vomp.fem.simulations.cube_fall import GroundCollidingSim
+
+
+from vomp.fem.fem_examples.mfem.softbody_sim import run_softbody_sim
+from vomp.fem.fem_examples.mfem.softbody_sim import ClassicFEM
+
+
+from vomp.fem.simulations.object_simulation import (
+ load_usd_mesh,
+ merge_meshes,
+ voxelize_mesh,
+ build_tet_mesh,
+)
+
+
+def compute_usd_bounds(file_path):
+ """Return axis-aligned bounding box (min, max) of all meshes inside a USD file.
+
+ Parameters
+ ----------
+ file_path : str
+ Path to a .usd or .usda file containing Mesh prims.
+
+ Returns
+ -------
+ tuple[np.ndarray, np.ndarray]
+ (min_bounds, max_bounds) each shape (3,), dtype float32
+ """
+ try:
+ from pxr import Usd, UsdGeom
+ except ImportError:
+ raise RuntimeError(
+ "pxr module not found. Please install the USD Python bindings (pip install usd-core)."
+ )
+
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"USD file '{file_path}' not found")
+
+ stage = Usd.Stage.Open(file_path)
+ if stage is None:
+ raise RuntimeError(f"Failed to open USD stage '{file_path}'")
+
+ bb_min = np.array([np.inf, np.inf, np.inf], dtype=np.float32)
+ bb_max = np.array([-np.inf, -np.inf, -np.inf], dtype=np.float32)
+
+ for prim in Usd.PrimRange(stage.GetPseudoRoot()):
+ if prim.IsA(UsdGeom.Mesh):
+ mesh = UsdGeom.Mesh(prim)
+ points_attr = mesh.GetPointsAttr()
+ if not points_attr.HasAuthoredValue():
+ continue
+ verts = points_attr.Get()
+ if verts is None:
+ continue
+ pts = np.asarray([(v[0], v[1], v[2]) for v in verts], dtype=np.float32)
+ bb_min = np.minimum(bb_min, pts.min(axis=0))
+ bb_max = np.maximum(bb_max, pts.max(axis=0))
+
+ if np.any(bb_min == np.inf):
+ raise RuntimeError("No Mesh prims with point data found in USD file")
+
+ return bb_min, bb_max
+
+
+def normalize_meshes(meshes, target_size=1.0, center=True):
+ """Normalize meshes to fit within a cube of target_size.
+
+ Parameters
+ ----------
+ meshes : list
+ List of (vertices, faces, name) tuples
+ target_size : float
+ Target size for the largest dimension of the bounding box
+ center : bool
+ Whether to center the meshes at origin
+
+ Returns
+ -------
+ list
+ Normalized meshes with same structure as input
+ """
+
+ all_vertices = []
+ for vtx, _, _ in meshes:
+ all_vertices.append(vtx)
+
+ if not all_vertices:
+ return meshes
+
+ all_verts_concat = np.concatenate(all_vertices, axis=0)
+ min_bounds = all_verts_concat.min(axis=0)
+ max_bounds = all_verts_concat.max(axis=0)
+
+ center_point = (min_bounds + max_bounds) / 2.0
+ size = max_bounds - min_bounds
+ max_size = size.max()
+
+ if max_size < 1e-6:
+ print("Warning: Object has near-zero size, skipping normalization")
+ return meshes
+
+ scale = target_size / max_size
+
+ normalized_meshes = []
+ for vtx, faces, name in meshes:
+ new_vtx = vtx.copy()
+ if center:
+ new_vtx -= center_point
+ new_vtx *= scale
+ normalized_meshes.append((new_vtx, faces, name))
+
+ print(f"Normalized object: scale={scale:.4f}, original_size={max_size:.4f}")
+ return normalized_meshes
+
+
+def main():
+ wp.init()
+
+ parser = argparse.ArgumentParser(
+ description="Simple drop simulation of a USD mesh using sparse voxels"
+ )
+ parser.add_argument(
+ "--usd_file",
+ type=str,
+ required=True,
+ help="Path to the USD file to drop",
+ )
+ parser.add_argument(
+ "--voxel_size",
+ type=float,
+ default=0.05,
+ help="Size of voxels used for sparse voxel simulation",
+ )
+ parser.add_argument(
+ "--ui",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Enable Polyscope visualisation",
+ )
+
+ parser.add_argument(
+ "--normalize",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Normalize the object to fit within a target size",
+ )
+ parser.add_argument(
+ "--target_size",
+ type=float,
+ default=1.0,
+ help="Target size for normalization (largest dimension)",
+ )
+ parser.add_argument(
+ "--center_object",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="Center the object at origin during normalization",
+ )
+
+ parser.add_argument(
+ "--youngs",
+ type=float,
+ nargs="+",
+ default=[1e4],
+ help="Young's modulus per mesh in order",
+ )
+ parser.add_argument(
+ "--poissons",
+ type=float,
+ nargs="+",
+ default=[0.45],
+ help="Poisson ratio per mesh in order",
+ )
+ parser.add_argument(
+ "--densities",
+ type=float,
+ nargs="+",
+ default=[500.0],
+ help="Density per mesh in order (not used yet)",
+ )
+
+ GroundCollidingSim.add_parser_arguments(parser)
+
+ parser.set_defaults(
+ n_newton=10,
+ n_frames=50,
+ young_modulus=1e4,
+ poisson_ratio=0.45,
+ density=500.0,
+ gravity=9.81,
+ ground=True,
+ ground_height=0.0,
+ collision_radius=0.05,
+ dt=0.02,
+ )
+
+ args = parser.parse_args()
+
+ usd_meshes = load_usd_mesh(args.usd_file)
+
+ print("Meshes found (index : name):")
+ for idx, (_, _, name) in enumerate(usd_meshes):
+ print(f" [{idx}] {name}")
+
+ n_mesh = len(usd_meshes)
+
+ if args.normalize:
+ usd_meshes = normalize_meshes(usd_meshes, args.target_size, args.center_object)
+
+ args.voxel_size = args.voxel_size * args.target_size
+
+ def _match_list(lst, fill):
+ if len(lst) >= n_mesh:
+ return lst[:n_mesh]
+ else:
+ return lst + [fill] * (n_mesh - len(lst))
+
+ young_list = _match_list(args.youngs, args.youngs[-1])
+ pois_list = _match_list(args.poissons, args.poissons[-1])
+ dens_list = _match_list(args.densities, args.densities[-1])
+
+ voxel_centers_all = []
+ mesh_indices = []
+
+ for m_idx, (vtx, faces, _name) in enumerate(usd_meshes):
+ vc, _ = voxelize_mesh(vtx, faces, voxel_size=args.voxel_size)
+ if vc is not None and len(vc):
+ voxel_centers_all.append(vc)
+ mesh_indices.append(np.full(len(vc), m_idx, dtype=np.int32))
+
+ if not voxel_centers_all:
+ raise RuntimeError("Voxelisation produced no voxels โ cannot proceed")
+
+ voxel_centers = np.concatenate(voxel_centers_all, axis=0)
+ voxel_mesh_idx = np.concatenate(mesh_indices, axis=0)
+
+ voxel_centers = voxel_centers[:, [0, 2, 1]]
+ voxel_centers[:, 2] *= -1.0
+
+ min_y = voxel_centers[:, 1].min()
+ lift_amount = 0.5 - min_y
+ voxel_centers[:, 1] += lift_amount
+
+ print(f"Total voxel centers: {len(voxel_centers)}")
+
+ pts = wp.array(voxel_centers, dtype=wp.vec3)
+
+ volume = wp.Volume.allocate_by_voxels(pts, voxel_size=args.voxel_size)
+ geo = fem.Nanogrid(volume)
+
+ print(f"Created sparse nanogrid with {geo.cell_count()} cells")
+
+ sim = GroundCollidingSim(geo, active_cells=None, args=args)
+ sim.init_displacement_space()
+ sim.init_strain_spaces()
+ sim.init_collision_detector()
+
+ def lame_from_E_nu(E, nu):
+ lam = E * nu / ((1 + nu) * (1 - 2 * nu))
+ mu = E / (2 * (1 + nu))
+ return lam, mu
+
+ print(f"\n=== MATERIAL ASSIGNMENT DEBUG ===")
+ print(f"Number of simulation cells: {geo.cell_count()}")
+ print(f"Number of original voxels: {len(voxel_centers)}")
+ print(f"Young's modulus values: {young_list}")
+ print(
+ f"Mesh indices per voxel: min={voxel_mesh_idx.min()}, max={voxel_mesh_idx.max()}"
+ )
+
+ from scipy.spatial import KDTree
+
+ voxel_tree = KDTree(voxel_centers)
+
+ n_cells = geo.cell_count()
+
+ print("Getting cell positions from volume structure...")
+
+ volume_voxels = wp.zeros(shape=(volume.get_voxel_count(),), dtype=wp.vec3i)
+ volume.get_voxels(volume_voxels)
+ volume_voxels_np = volume_voxels.numpy()
+
+ print(f"Volume created {len(volume_voxels_np)} voxels")
+ print(f"Volume voxel size: {args.voxel_size}")
+
+ cell_positions = volume_voxels_np.astype(np.float32) * args.voxel_size
+
+ cell_positions += args.voxel_size * 0.5
+
+ print(
+ f"Cell positions range: min={cell_positions.min(axis=0)}, max={cell_positions.max(axis=0)}"
+ )
+ print(
+ f"Original voxel range: min={voxel_centers.min(axis=0)}, max={voxel_centers.max(axis=0)}"
+ )
+
+ if len(cell_positions) != n_cells:
+ print(
+ f"WARNING: Mismatch between volume voxels ({len(cell_positions)}) and nanogrid cells ({n_cells})"
+ )
+
+ node_positions = sim.u_field.space.node_positions().numpy()
+ cell_node_indices = sim.u_field.space.topology.element_node_indices().numpy()
+
+ cell_positions = np.zeros((n_cells, 3), dtype=np.float32)
+ for cell_idx in range(n_cells):
+ node_indices = cell_node_indices[cell_idx]
+ cell_nodes = node_positions[node_indices]
+ cell_positions[cell_idx] = np.mean(cell_nodes, axis=0)
+ print("Using fallback method: computing cell centers from node positions")
+
+ lame_np = np.zeros((n_cells, 2), dtype=np.float32)
+
+ material_counts = np.zeros(len(young_list), dtype=int)
+
+ max_distance = 0.0
+ total_distance = 0.0
+
+ print("Assigning materials to cells...")
+
+ for cell_idx in range(min(n_cells, len(cell_positions))):
+ cell_center = cell_positions[cell_idx]
+
+ distance, closest_voxel_idx = voxel_tree.query(cell_center)
+ mesh_idx = voxel_mesh_idx[closest_voxel_idx]
+
+ lam, mu = lame_from_E_nu(young_list[mesh_idx], pois_list[mesh_idx])
+ lame_np[cell_idx, 0] = lam
+ lame_np[cell_idx, 1] = mu
+
+ material_counts[mesh_idx] += 1
+ max_distance = max(max_distance, distance)
+ total_distance += distance
+
+ if cell_idx < 5:
+ print(
+ f"Cell {cell_idx}: center={cell_center}, closest_voxel={closest_voxel_idx}, "
+ f"distance={distance:.4f}, mesh_idx={mesh_idx}, E={young_list[mesh_idx]:.0e}"
+ )
+
+ if len(cell_positions) < n_cells:
+ print(
+ f"WARNING: {n_cells - len(cell_positions)} cells may not have been assigned materials"
+ )
+
+ default_lam, default_mu = lame_from_E_nu(young_list[0], pois_list[0])
+ for cell_idx in range(len(cell_positions), n_cells):
+ lame_np[cell_idx, 0] = default_lam
+ lame_np[cell_idx, 1] = default_mu
+
+ print(f"\n=== MATERIAL ASSIGNMENT RESULTS ===")
+ print(f"Maximum distance to closest voxel: {max_distance:.4f}")
+ print(f"Average distance to closest voxel: {total_distance / n_cells:.4f}")
+ print(f"Material distribution: {material_counts}")
+
+ zero_lam = np.sum(lame_np[:, 0] == 0)
+ zero_mu = np.sum(lame_np[:, 1] == 0)
+ if zero_lam > 0 or zero_mu > 0:
+ print(
+ f"WARNING: Found {zero_lam} cells with zero lambda, {zero_mu} cells with zero mu"
+ )
+
+ print(f"Lame field expects {sim.lame_field.dof_values.shape[0]} values")
+ print(f"We have {len(lame_np)} material assignments")
+
+ if sim.lame_field.dof_values.shape[0] != len(lame_np):
+ print(
+ f"ERROR: Field size mismatch! Expected {sim.lame_field.dof_values.shape[0]}, got {len(lame_np)}"
+ )
+ print("This suggests the material field structure doesn't match the geometry")
+
+ if len(lame_np) > sim.lame_field.dof_values.shape[0]:
+ print("Truncating material array to match field size")
+ lame_np = lame_np[: sim.lame_field.dof_values.shape[0]]
+ else:
+ print("Extending material array to match field size")
+
+ additional_entries = sim.lame_field.dof_values.shape[0] - len(lame_np)
+ last_lam, last_mu = lame_from_E_nu(young_list[-1], pois_list[-1])
+ last_material = np.array([last_lam, last_mu], dtype=np.float32)
+ extension = np.tile(last_material, (additional_entries, 1))
+ lame_np = np.vstack([lame_np, extension])
+
+ sim.lame_field.dof_values.assign(wp.array(lame_np, dtype=wp.vec2))
+
+ E_cells = (lame_np[:, 1] * (3 * lame_np[:, 0] + 2 * lame_np[:, 1])) / (
+ lame_np[:, 0] + lame_np[:, 1] + 1e-9
+ )
+ max_E = float(E_cells.max())
+ min_E = float(E_cells.min())
+ print(f"Final Young's modulus range: {min_E:.3e} โ {max_E:.3e} Pa")
+ print(f"Material assignment completed successfully!\n")
+
+ E_cells = (lame_np[:, 1] * (3 * lame_np[:, 0] + 2 * lame_np[:, 1])) / (
+ lame_np[:, 0] + lame_np[:, 1] + 1e-9
+ )
+ max_E = float(E_cells.max())
+ min_E = float(E_cells.min())
+ print(f"Young's modulus range across cells: {min_E:.3e} โ {max_E:.3e}")
+
+ sim.typical_stiffness = max(
+ args.density * args.gravity * sim.typical_length,
+ min(max_E, args.density * sim.typical_length**2 / (args.dt**2)),
+ )
+
+ sim.set_boundary_condition(boundary_projector_form=None)
+
+ merged_all_vertices, merged_all_faces = merge_meshes(usd_meshes)
+
+ surf_vertices = merged_all_vertices.copy()
+
+ rot_surf = surf_vertices.copy()
+ rot_surf[:, 1], rot_surf[:, 2] = surf_vertices[:, 2], -surf_vertices[:, 1]
+ rot_surf[:, 1] += lift_amount
+
+ surf_vertices = rot_surf
+ surf_faces = merged_all_faces.astype(np.int32)
+
+ max_valid = surf_vertices.shape[0] - 1
+ surf_faces = np.where(surf_faces > max_valid, max_valid, surf_faces)
+
+ recorded = []
+
+ def _recorder(pos):
+ recorded.append(pos.copy())
+
+ run_softbody_sim(sim, ui=False, frame_callback=_recorder)
+
+ if not args.ui:
+ return
+
+ import polyscope as ps
+ import polyscope.imgui as psim
+
+ ps.init()
+ ps.set_ground_plane_mode("tile")
+ ps.set_ground_plane_height(0.0)
+
+ surf_mesh = ps.register_surface_mesh(
+ "usd_mesh",
+ surf_vertices,
+ surf_faces,
+ edge_width=1.0,
+ )
+
+ sim_nodes = recorded[0]
+
+ lame_values = sim.lame_field.dof_values.numpy()
+
+ E_cells = (lame_values[:, 1] * (3 * lame_values[:, 0] + 2 * lame_values[:, 1])) / (
+ lame_values[:, 0] + lame_values[:, 1] + 1e-9
+ )
+
+ print(
+ f"Young's modulus range for visualization: {E_cells.min():.2e} - {E_cells.max():.2e} Pa"
+ )
+
+ cell_node_indices = sim.u_field.space.topology.element_node_indices().numpy()
+
+ print(
+ f"Debug: E_cells shape: {E_cells.shape}, cell_node_indices shape: {cell_node_indices.shape}"
+ )
+
+ node_to_E = np.zeros(sim_nodes.shape[0])
+
+ n_cells_to_process = min(len(E_cells), cell_node_indices.shape[0])
+ print(f"Processing {n_cells_to_process} cells")
+
+ for cell_idx in range(n_cells_to_process):
+ node_indices = cell_node_indices[cell_idx]
+ node_to_E[node_indices] = E_cells[cell_idx]
+
+ E_min, E_max = E_cells.min(), E_cells.max()
+
+ E_log = np.log10(node_to_E + 1e-9)
+ E_log_min, E_log_max = np.log10(E_min), np.log10(E_max)
+ E_normalized = (E_log - E_log_min) / (E_log_max - E_log_min + 1e-9)
+
+ import matplotlib.pyplot as plt
+ import matplotlib.cm as cm
+
+ colormap = cm.get_cmap("viridis")
+ colors = colormap(E_normalized)[:, :3]
+
+ print(f"Colored {len(sim_nodes)} physics voxels based on Young's modulus")
+ print(
+ f"Color mapping (log scale): Blue = {E_min:.2e} Pa (soft), Yellow = {E_max:.2e} Pa (stiff)"
+ )
+ print(f"Log range: {E_log_min:.2f} - {E_log_max:.2f}")
+
+ unique_colors = np.unique(E_normalized)
+ print(f"Number of unique color values: {len(unique_colors)}")
+ if len(unique_colors) <= 10:
+ print(f"Color values: {unique_colors}")
+
+ physics_voxels = ps.register_point_cloud(
+ "physics_voxels",
+ sim_nodes,
+ radius=args.voxel_size * 0.3,
+ )
+ physics_voxels.add_color_quantity("youngs_modulus", colors, enabled=True)
+ physics_voxels.set_enabled(True)
+
+ try:
+
+ if hasattr(geo, "cell_positions"):
+ cell_centers = geo.cell_positions().numpy()
+ volume_viz = ps.register_point_cloud(
+ "volume_cells",
+ cell_centers,
+ radius=args.voxel_size * 0.4,
+ )
+ volume_viz.set_color((0.3, 0.8, 0.3))
+ volume_viz.set_enabled(False)
+ except:
+
+ pass
+
+ from scipy.spatial import KDTree
+
+ kdtree = KDTree(recorded[0])
+
+ surf_to_node = [kdtree.query(v)[1] for v in surf_vertices]
+
+ current = [0]
+ play = [False]
+ last = [time.time()]
+ fps = [20]
+
+ surf_enabled = [True]
+ physics_enabled = [True]
+
+ def _ui():
+ changed, val = psim.SliderInt("frame", current[0], 0, len(recorded) - 1)
+ if changed:
+ current[0] = val
+ _update_frame(val)
+
+ changed_fps, new_fps = psim.SliderInt("fps", fps[0], 1, 60)
+ if changed_fps:
+ fps[0] = new_fps
+
+ if psim.Button("Play" if not play[0] else "Pause"):
+ play[0] = not play[0]
+ last[0] = time.time()
+
+ if psim.Button("Toggle Surface"):
+ surf_enabled[0] = not surf_enabled[0]
+ surf_mesh.set_enabled(surf_enabled[0])
+
+ if psim.Button("Toggle Physics Voxels"):
+ physics_enabled[0] = not physics_enabled[0]
+ physics_voxels.set_enabled(physics_enabled[0])
+
+ if play[0] and time.time() - last[0] > 1.0 / fps[0]:
+ current[0] = (current[0] + 1) % len(recorded)
+ _update_frame(current[0])
+ last[0] = time.time()
+
+ def _update_frame(idx):
+ disp = recorded[idx] - recorded[0]
+
+ surf_mesh.update_vertex_positions(surf_vertices + disp[surf_to_node])
+
+ physics_voxels.update_point_positions(recorded[idx])
+
+ ps.set_user_callback(_ui)
+ ps.show()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/inference/__init__.py b/deps/vomp/vomp/inference/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c20daa991d680036301443b055eb95c3642705c3
--- /dev/null
+++ b/deps/vomp/vomp/inference/__init__.py
@@ -0,0 +1,34 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Inference utilities for VoMP material property estimation.
+
+This module provides the core inference capabilities for estimating material properties
+from 3D representations using vision transformers and neural networks.
+"""
+
+from .utils import LazyLoadDino, MaterialUpsampler, save_materials, load_materials
+from .vomp import Vomp
+from .replicator_renderer import RTX_PRESETS
+
+__all__ = [
+ "LazyLoadDino",
+ "MaterialUpsampler",
+ "save_materials",
+ "load_materials",
+ "Vomp",
+ "RTX_PRESETS",
+]
diff --git a/deps/vomp/vomp/inference/ply_utils.py b/deps/vomp/vomp/inference/ply_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..d75cd17187dcc47def74f49005d8a222a83c2cef
--- /dev/null
+++ b/deps/vomp/vomp/inference/ply_utils.py
@@ -0,0 +1,176 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+
+
+def write_ply_vertices(
+ vertices: np.ndarray, output_path: str, binary: bool = True
+) -> None:
+ """
+ Write vertex coordinates to a PLY file.
+
+ Args:
+ vertices: Array of 3D vertex positions (N, 3) as float32
+ output_path: Path to output PLY file
+ binary: If True, write binary little-endian format (default).
+ If False, write ASCII format.
+ """
+ n_vertices = len(vertices)
+
+ if n_vertices > 0:
+ vertices = np.asarray(vertices, dtype=np.float32)
+ if vertices.ndim == 1:
+ vertices = vertices.reshape(-1, 3)
+ assert (
+ vertices.shape[1] == 3
+ ), f"Expected (N, 3) array, got shape {vertices.shape}"
+
+ format_str = "binary_little_endian 1.0" if binary else "ascii 1.0"
+ header = f"""ply
+format {format_str}
+element vertex {n_vertices}
+property float x
+property float y
+property float z
+end_header
+"""
+
+ with open(output_path, "wb") as f:
+ f.write(header.encode("ascii"))
+
+ if n_vertices > 0:
+ if binary:
+ vertices.astype(" np.ndarray:
+ """
+ Read vertex coordinates from a PLY file.
+
+ Supports ASCII and binary (little-endian and big-endian) PLY formats.
+ Only reads x, y, z float properties from the vertex element.
+
+ Args:
+ input_path: Path to input PLY file
+
+ Returns:
+ Array of 3D vertex positions (N, 3) as float32
+ """
+ with open(input_path, "rb") as f:
+ header_lines = []
+ while True:
+ line = f.readline().decode("ascii").strip()
+ header_lines.append(line)
+ if line == "end_header":
+ break
+
+ format_type = None
+ n_vertices = 0
+ properties = []
+ in_vertex_element = False
+
+ for line in header_lines:
+ parts = line.split()
+ if not parts:
+ continue
+
+ if parts[0] == "format":
+ format_type = parts[1]
+ elif parts[0] == "element" and parts[1] == "vertex":
+ n_vertices = int(parts[2])
+ in_vertex_element = True
+ elif parts[0] == "element" and parts[1] != "vertex":
+ in_vertex_element = False
+ elif parts[0] == "property" and in_vertex_element:
+ prop_type = parts[1]
+ prop_name = parts[2]
+ properties.append((prop_name, prop_type))
+
+ if n_vertices == 0:
+ return np.empty((0, 3), dtype=np.float32)
+
+ prop_info = {}
+ for i, (name, ptype) in enumerate(properties):
+ if name in ("x", "y", "z"):
+ prop_info[name] = (i, ptype)
+
+ if not all(name in prop_info for name in ("x", "y", "z")):
+ raise ValueError("PLY file must have x, y, z properties in vertex element")
+
+ type_map = {
+ "float": (""
+
+ dtype_list = []
+ for name, ptype in properties:
+ if ptype not in type_map:
+ raise ValueError(f"Unsupported property type: {ptype}")
+ np_dtype, _ = type_map[ptype]
+ if endian == ">":
+ np_dtype = ">" + np_dtype[1:]
+ dtype_list.append((name, np_dtype))
+
+ vertex_dtype = np.dtype(dtype_list)
+ data = np.frombuffer(
+ f.read(n_vertices * vertex_dtype.itemsize), dtype=vertex_dtype
+ )
+
+ vertices = np.column_stack(
+ [
+ data["x"].astype(np.float32),
+ data["y"].astype(np.float32),
+ data["z"].astype(np.float32),
+ ]
+ )
+ else:
+ raise ValueError(f"Unsupported PLY format: {format_type}")
+
+ return vertices
diff --git a/deps/vomp/vomp/inference/replicator_renderer.py b/deps/vomp/vomp/inference/replicator_renderer.py
new file mode 100644
index 0000000000000000000000000000000000000000..64a2bd24265988d586a185801c66d34b5fc568a9
--- /dev/null
+++ b/deps/vomp/vomp/inference/replicator_renderer.py
@@ -0,0 +1,439 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Replicator-based rendering for VoMP material inference.
+
+This module provides an alternative to Blender rendering using NVIDIA Isaac Sim's
+Replicator for photorealistic rendering of 3D assets.
+"""
+
+import os
+import json
+import tempfile
+import subprocess
+from typing import Dict, List, Optional, Any, Union
+import numpy as np
+
+# Preset RTX configurations
+RTX_PRESETS = {
+ "fast": {
+ "/rtx/rendermode": "RayTracedLighting", # Real-time ray tracing
+ "/rtx/post/backgroundZeroAlpha/enabled": True,
+ "/rtx/post/backgroundZeroAlpha/backgroundComposite": False,
+ "/rtx/post/backgroundZeroAlpha/outputAlphaInComposite": True,
+ },
+ "path_tracing": {
+ "/rtx/rendermode": "PathTracing",
+ "/rtx/pathtracing/spp": 256,
+ "/rtx/pathtracing/totalSpp": 256,
+ "/rtx/pathtracing/maxBounces": 8,
+ "/rtx/pathtracing/maxSpecularAndTransmissionBounces": 8,
+ "/rtx/post/backgroundZeroAlpha/enabled": True,
+ "/rtx/post/backgroundZeroAlpha/backgroundComposite": False,
+ "/rtx/post/backgroundZeroAlpha/outputAlphaInComposite": True,
+ "/rtx/pathtracing/fireflyFilter/enable": True,
+ "/rtx/pathtracing/optixDenoiser/enabled": True,
+ },
+}
+
+
+def generate_replicator_script(
+ asset_path: str,
+ output_dir: str,
+ num_views: int,
+ resolution: tuple,
+ yaws: List[float],
+ pitchs: List[float],
+ radius_list: List[float],
+ fov_list: List[float],
+ rtx_settings: Dict[str, Any],
+ light_intensity: float = 1000.0,
+ normalize_object: bool = True,
+) -> str:
+ """
+ Generate a Replicator rendering script for Isaac Sim execution.
+
+ Args:
+ asset_path: Path to USD asset file
+ output_dir: Directory to save renders
+ num_views: Number of views to render
+ resolution: Tuple of (width, height)
+ yaws: List of yaw angles
+ pitchs: List of pitch angles
+ radius_list: List of camera radii
+ fov_list: List of FOV values
+ rtx_settings: RTX renderer settings dictionary
+ light_intensity: Sphere light intensity
+ normalize_object: Whether to normalize object to [-0.5, 0.5]
+
+ Returns:
+ String containing the complete Python script
+ """
+ script = f'''"""
+Auto-generated Replicator rendering script for VoMP.
+Generated for asset: {os.path.basename(asset_path)}
+"""
+
+import os
+import omni.replicator.core as rep
+import omni.replicator.core.functional as F
+from PIL import Image
+import carb
+import numpy as np
+import math
+import omni.usd
+from pxr import UsdGeom, Gf, Usd
+import json
+
+# Configuration
+RESOLUTION = {resolution}
+ASSET_PATH = "{asset_path}"
+OUTPUT_DIR = "{output_dir}"
+NUM_CAMERAS = {num_views}
+LIGHT_INTENSITY = {light_intensity}
+NORMALIZE_OBJECT = {normalize_object}
+
+# Camera parameters (pre-computed)
+YAWS = {yaws}
+PITCHS = {pitchs}
+RADIUS_LIST = {radius_list}
+FOV_LIST = {fov_list}
+
+# Create output directory
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+renders_dir = os.path.join(OUTPUT_DIR, "renders")
+os.makedirs(renders_dir, exist_ok=True)
+
+# RTX Settings
+RTX_SETTINGS = {rtx_settings}
+
+print("Configuring RTX renderer...")
+for setting_path, value in RTX_SETTINGS.items():
+ carb.settings.get_settings().set(setting_path, value)
+print("โ RTX settings applied")
+
+# Set stage Z-Up
+rep.settings.set_stage_up_axis("Z")
+rep.settings.set_stage_meters_per_unit(1.0)
+
+
+def normalize_asset_to_unit_box(asset_prim_path, target_scale=0.98):
+ """Normalize asset to fit within [-0.5, 0.5] coordinate space."""
+ stage = omni.usd.get_context().get_stage()
+ prim = stage.GetPrimAtPath(asset_prim_path)
+
+ if not prim:
+ print(f"Warning: Could not find prim at {{asset_prim_path}}")
+ return 1.0, (0, 0, 0)
+
+ # Compute bounding box
+ bbox_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), ["default", "render"])
+ bbox = bbox_cache.ComputeWorldBound(prim)
+ bbox_range = bbox.GetRange()
+
+ min_point = bbox_range.GetMin()
+ max_point = bbox_range.GetMax()
+
+ center = (min_point + max_point) / 2.0
+ extent_vec = max_point - min_point
+ max_extent = max(extent_vec[0], extent_vec[1], extent_vec[2])
+
+ if max_extent == 0:
+ print("Warning: Object has zero extent")
+ return 1.0, (0, 0, 0)
+
+ scale_factor = target_scale / max_extent
+
+ print(f"Object normalization:")
+ print(f" Max extent: {{max_extent:.3f}}")
+ print(f" Scale factor: {{scale_factor:.6f}}")
+
+ return scale_factor, center
+
+
+# Import asset
+print(f"Loading asset: {{ASSET_PATH}}")
+asset = F.create.reference(ASSET_PATH)
+
+# Normalize if requested
+if NORMALIZE_OBJECT:
+ stage = omni.usd.get_context().get_stage()
+ asset_prim_path = asset.GetPath()
+
+ scale_factor, center = normalize_asset_to_unit_box(asset_prim_path, target_scale=0.98)
+
+ xformable = UsdGeom.Xformable(stage.GetPrimAtPath(asset_prim_path))
+ xformable.ClearXformOpOrder()
+
+ scale_op = xformable.AddScaleOp()
+ scale_op.Set(Gf.Vec3f(scale_factor, scale_factor, scale_factor))
+
+ translate_op = xformable.AddTranslateOp()
+ translate_op.Set(Gf.Vec3f(-center[0] * scale_factor, -center[1] * scale_factor, -center[2] * scale_factor))
+
+ print("โ Asset normalized to [-0.5, 0.5]")
+
+# Setup annotator
+anno = rep.annotators.get("rgb")
+
+# Store frame metadata
+frames_metadata = []
+
+
+async def main():
+ """Render all camera views."""
+ print(f"\\nRendering {{NUM_CAMERAS}} views...")
+
+ for i in range(NUM_CAMERAS):
+ yaw = YAWS[i]
+ pitch = PITCHS[i]
+ radius = RADIUS_LIST[i]
+ fov = FOV_LIST[i]
+
+ # Convert spherical to Cartesian
+ x = radius * math.cos(pitch) * math.cos(yaw)
+ y = radius * math.cos(pitch) * math.sin(yaw)
+ z = radius * math.sin(pitch)
+
+ # Create camera
+ camera = F.create.camera(position=[x, y, z], look_at=asset)
+
+ # Add light
+ light = F.create.sphere_light(parent=camera, intensity=LIGHT_INTENSITY)
+
+ # Create render product
+ render_product = rep.create.render_product(camera, RESOLUTION)
+ anno.attach(render_product)
+
+ # Render
+ await rep.orchestrator.step_async()
+
+ # Save image
+ filename = f"{{i:04d}}.png"
+ output_path = os.path.join(renders_dir, filename)
+ Image.fromarray(anno.get_data()).save(output_path)
+
+ # Create camera transform matrix for metadata
+ # Camera to world transform
+ cam_pos = np.array([x, y, z])
+
+ # Look-at transformation (simplified)
+ forward = -cam_pos / np.linalg.norm(cam_pos)
+ right = np.cross([0, 0, 1], forward)
+ if np.linalg.norm(right) < 1e-6:
+ right = np.cross([0, 1, 0], forward)
+ right = right / np.linalg.norm(right)
+ up = np.cross(forward, right)
+
+ # Build camera-to-world matrix
+ c2w = np.eye(4)
+ c2w[:3, 0] = right
+ c2w[:3, 1] = up
+ c2w[:3, 2] = forward
+ c2w[:3, 3] = cam_pos
+
+ # Convert to NeRF convention (flip Y and Z)
+ c2w[:3, 1:3] *= -1
+
+ frame_metadata = {{
+ "file_path": filename,
+ "transform_matrix": c2w.tolist(),
+ "camera_angle_x": np.radians(fov),
+ "yaw": float(yaw),
+ "pitch": float(pitch),
+ "radius": float(radius),
+ }}
+ frames_metadata.append(frame_metadata)
+
+ print(f" Rendered view {{i+1}}/{{NUM_CAMERAS}}")
+
+ # Clean up
+ stage = omni.usd.get_context().get_stage()
+ if stage.GetPrimAtPath(camera.GetPath()):
+ stage.RemovePrim(camera.GetPath())
+ if stage.GetPrimAtPath(light.GetPath()):
+ stage.RemovePrim(light.GetPath())
+
+ # Save transforms.json
+ transforms_data = {{
+ "camera_angle_x": np.radians(FOV_LIST[0]) if FOV_LIST else 0.0,
+ "frames": frames_metadata,
+ }}
+
+ transforms_path = os.path.join(renders_dir, "transforms.json")
+ with open(transforms_path, "w") as f:
+ json.dump(transforms_data, f, indent=2)
+
+ # Save metadata for pipeline compatibility
+ metadata_path = os.path.join(OUTPUT_DIR, "renders_metadata.json")
+ with open(metadata_path, "w") as f:
+ json.dump(frames_metadata, f, indent=2)
+
+ print(f"\\nโ All {{NUM_CAMERAS}} renders completed!")
+ print(f"โ Output: {{OUTPUT_DIR}}")
+
+ # Exit
+ carb.settings.get_settings().set("/app/file/ignoreUnsavedOnExit", True)
+ omni.kit.app.get_app().post_quit()
+
+
+import asyncio
+asyncio.ensure_future(main())
+'''
+ return script
+
+
+def render_with_replicator(
+ asset_path: str,
+ output_dir: str,
+ num_views: int,
+ yaws: List[float],
+ pitchs: List[float],
+ radius_list: List[float],
+ fov_list: List[float],
+ isaac_sim_path: str,
+ resolution: tuple = (1024, 1024),
+ render_mode: str = "path_tracing",
+ rtx_settings_override: Optional[Dict[str, Any]] = None,
+ light_intensity: float = 1000.0,
+ normalize_object: bool = True,
+) -> List[Dict[str, Any]]:
+ """
+ Render views using Isaac Sim Replicator in headless mode.
+
+ Args:
+ asset_path: Path to USD asset file (.usd, .usda, .usdc)
+ output_dir: Directory to save rendered images
+ num_views: Number of camera views to render
+ yaws: List of yaw angles (azimuth) for camera positions
+ pitchs: List of pitch angles (elevation) for camera positions
+ radius_list: List of camera distances from object
+ fov_list: List of field-of-view values (degrees)
+ isaac_sim_path: Path to Isaac Sim executable (isaac-sim.sh)
+ resolution: Tuple of (width, height) for rendered images
+ render_mode: Rendering quality preset - "fast" or "path_tracing"
+ rtx_settings_override: Optional dict to override RTX settings
+ light_intensity: Sphere light intensity value
+ normalize_object: Whether to normalize object to [-0.5, 0.5]
+
+ Returns:
+ List of frame metadata dictionaries with camera transforms
+
+ Raises:
+ FileNotFoundError: If Isaac Sim executable not found
+ RuntimeError: If rendering fails
+
+ Example:
+ >>> yaws, pitchs, radii, fovs = sample_camera_views(150)
+ >>> frames = render_with_replicator(
+ ... asset_path="model.usd",
+ ... output_dir="./renders",
+ ... num_views=150,
+ ... yaws=yaws, pitchs=pitchs,
+ ... radius_list=radii, fov_list=fovs,
+ ... isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ ... render_mode="path_tracing"
+ ... )
+ """
+ # Validate Isaac Sim path
+ isaac_sim_path = os.path.expanduser(isaac_sim_path)
+ if not os.path.exists(isaac_sim_path):
+ raise FileNotFoundError(
+ f"Isaac Sim executable not found at: {isaac_sim_path}\n"
+ f"Please provide the correct path to isaac-sim.sh"
+ )
+
+ # Get RTX settings
+ if render_mode not in RTX_PRESETS:
+ raise ValueError(
+ f"Invalid render_mode: {render_mode}. Must be 'fast' or 'path_tracing'"
+ )
+
+ rtx_settings = RTX_PRESETS[render_mode].copy()
+
+ # Apply user overrides
+ if rtx_settings_override:
+ rtx_settings.update(rtx_settings_override)
+
+ print(f"=== Replicator Rendering with Isaac Sim ===")
+ print(f"Render mode: {render_mode}")
+ print(f"Asset: {os.path.basename(asset_path)}")
+ print(f"Views: {num_views}")
+ print(f"Resolution: {resolution[0]}x{resolution[1]}")
+
+ # Generate rendering script
+ script_content = generate_replicator_script(
+ asset_path=os.path.abspath(asset_path),
+ output_dir=os.path.abspath(output_dir),
+ num_views=num_views,
+ resolution=resolution,
+ yaws=yaws,
+ pitchs=pitchs,
+ radius_list=radius_list,
+ fov_list=fov_list,
+ rtx_settings=rtx_settings,
+ light_intensity=light_intensity,
+ normalize_object=normalize_object,
+ )
+
+ # Create temporary script file
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".py", delete=False, dir=output_dir
+ ) as f:
+ script_path = f.name
+ f.write(script_content)
+
+ print(f"Generated rendering script: {script_path}")
+
+ try:
+ # Execute Isaac Sim in headless mode
+ print(f"Launching Isaac Sim in headless mode...")
+ cmd = [isaac_sim_path, "--no-window", "--exec", script_path]
+
+ result = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ timeout=3600, # 1 hour timeout
+ )
+
+ if result.returncode != 0:
+ print(f"Isaac Sim stderr:\n{result.stderr}")
+ raise RuntimeError(
+ f"Isaac Sim rendering failed with return code {result.returncode}"
+ )
+
+ print(f"โ Isaac Sim rendering completed")
+
+ # Load and return metadata
+ metadata_path = os.path.join(output_dir, "renders_metadata.json")
+ if os.path.exists(metadata_path):
+ with open(metadata_path, "r") as f:
+ frames_metadata = json.load(f)
+ print(f"โ Loaded metadata for {len(frames_metadata)} frames")
+ return frames_metadata
+ else:
+ raise RuntimeError(
+ f"Metadata file not found: {metadata_path}\n"
+ f"Isaac Sim may not have completed successfully"
+ )
+
+ finally:
+ # Clean up temporary script
+ if os.path.exists(script_path):
+ os.remove(script_path)
+ print(f"Cleaned up temporary script")
diff --git a/deps/vomp/vomp/inference/usd_utils.py b/deps/vomp/vomp/inference/usd_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..6dd807dd77d18846830d0b669d5adbaf0955b1a9
--- /dev/null
+++ b/deps/vomp/vomp/inference/usd_utils.py
@@ -0,0 +1,390 @@
+"""
+USD utilities for SimReady format USD files.
+"""
+
+import os
+import glob
+import numpy as np
+import shutil
+import tempfile
+import traceback
+from typing import Tuple, List, Dict, Optional
+from pxr import Usd, UsdGeom, UsdShade
+
+
+def find_textures_in_directory(model_dir: str) -> Dict[str, str]:
+ """Find texture files in the model directory and subdirectories."""
+ textures = {}
+ if not os.path.exists(model_dir):
+ return textures
+
+ prop_name = os.path.basename(model_dir)
+ parent_dir = os.path.dirname(model_dir)
+
+ texture_dirs = [
+ model_dir,
+ os.path.join(model_dir, "textures"),
+ os.path.join(model_dir, "Textures"),
+ os.path.join(parent_dir, "textures"),
+ os.path.join(parent_dir, "Textures"),
+ ]
+
+ texture_files = []
+ for texture_dir in texture_dirs:
+ if not os.path.exists(texture_dir):
+ continue
+ texture_files.extend(
+ glob.glob(os.path.join(texture_dir, "**/*.png"), recursive=True)
+ )
+ texture_files.extend(
+ glob.glob(os.path.join(texture_dir, "**/*.jpg"), recursive=True)
+ )
+
+ texture_candidates = {
+ "diffuse": [],
+ "normal": [],
+ "roughness": [],
+ "metallic": [],
+ "specular": [],
+ "bump": [],
+ }
+
+ for texture_path in texture_files:
+ filename = os.path.basename(texture_path).lower()
+
+ if any(term in filename for term in ["diffuse", "color", "albedo", "base"]):
+ texture_candidates["diffuse"].append(texture_path)
+ if any(term in filename for term in ["normal", "nrm"]):
+ texture_candidates["normal"].append(texture_path)
+ if any(term in filename for term in ["rough", "roughness"]):
+ texture_candidates["roughness"].append(texture_path)
+ if any(term in filename for term in ["metal", "metallic"]):
+ texture_candidates["metallic"].append(texture_path)
+ if any(term in filename for term in ["spec", "specular"]):
+ texture_candidates["specular"].append(texture_path)
+ if any(term in filename for term in ["bump", "height"]):
+ texture_candidates["bump"].append(texture_path)
+
+ for tex_type, candidates in texture_candidates.items():
+ if candidates:
+ textures[tex_type] = candidates[0]
+
+ return textures
+
+
+def extract_material_info(mesh_prim, stage) -> Dict:
+ """Extract material information from a USD mesh prim."""
+ material_info = {
+ "name": f"material_{mesh_prim.GetName()}",
+ "diffuse_color": [0.8, 0.8, 0.8],
+ "specular_color": [0.2, 0.2, 0.2],
+ "roughness": 0.5,
+ "metallic": 0.0,
+ "textures": {},
+ }
+
+ model_dir = os.path.dirname(stage.GetRootLayer().realPath)
+ material_info["textures"] = find_textures_in_directory(model_dir)
+
+ return material_info
+
+
+def load_mesh_from_usd(usd_file_path: str, temp_dir: Optional[str] = None):
+ """
+ Load and merge all meshes from a USD file with texture information.
+ """
+ if not os.path.exists(usd_file_path):
+ print(f"Error: USD file not found at {usd_file_path}")
+ return None, None, None, None, None
+
+ try:
+ stage = Usd.Stage.Open(usd_file_path)
+ if not stage:
+ print(f"Error: Failed to open USD stage from {usd_file_path}")
+ return None, None, None, None, None
+
+ default_prim = stage.GetDefaultPrim()
+ if not default_prim:
+ default_prim = stage.GetPrimAtPath("/")
+
+ mesh_prims = []
+ for prim in Usd.PrimRange(default_prim):
+ if prim.GetTypeName() == "Mesh":
+ mesh_prims.append(prim)
+
+ if not mesh_prims:
+ print(f"Error: No meshes found in {usd_file_path}")
+ return None, None, None, None, None
+
+ all_vertices = []
+ all_faces = []
+ all_materials = []
+ all_face_materials = []
+ all_uvs = [] # Flat list
+ vertex_offset = 0
+ uv_offset = 0 # Only used when has_uvs=True
+
+ for mesh_prim in mesh_prims:
+ mesh = UsdGeom.Mesh(mesh_prim)
+
+ points_attr = mesh.GetPointsAttr()
+ if not points_attr or not points_attr.Get():
+ continue
+
+ verts = points_attr.Get()
+ vertices = np.array([(v[0], v[1], v[2]) for v in verts], dtype=np.float32)
+
+ face_vertex_counts = mesh.GetFaceVertexCountsAttr().Get()
+ face_vertex_indices = mesh.GetFaceVertexIndicesAttr().Get()
+
+ if not face_vertex_counts or not face_vertex_indices:
+ continue
+
+ has_uvs = False
+ mesh_uvs = []
+
+ for primvar in [
+ UsdGeom.PrimvarsAPI(mesh_prim).GetPrimvar("st"),
+ UsdGeom.PrimvarsAPI(mesh_prim).GetPrimvar("uvs"),
+ UsdGeom.PrimvarsAPI(mesh_prim).GetPrimvar("uv"),
+ UsdGeom.PrimvarsAPI(mesh_prim).GetPrimvar("UVMap"),
+ UsdGeom.PrimvarsAPI(mesh_prim).GetPrimvar("texCoords"),
+ ]:
+ if primvar and primvar.Get():
+ uv_data = primvar.Get()
+ interp = primvar.GetInterpolation()
+
+ if interp == UsdGeom.Tokens.vertex:
+ mesh_uvs = np.array(
+ [(uv[0], uv[1]) for uv in uv_data], dtype=np.float32
+ )
+ has_uvs = True
+ break
+ elif interp == UsdGeom.Tokens.faceVarying:
+ mesh_uvs = np.array(
+ [(uv[0], uv[1]) for uv in uv_data], dtype=np.float32
+ )
+ has_uvs = True
+ break
+
+ if not has_uvs:
+ mesh_uvs = np.zeros((len(vertices), 2), dtype=np.float32)
+
+ # Extract material
+ try:
+ material_info = extract_material_info(mesh_prim, stage)
+ all_materials.append(material_info)
+ material_index = len(all_materials) - 1
+ except Exception as e:
+ print(f"Warning: Error extracting material: {e}")
+ material_info = {
+ "name": f"material_{mesh_prim.GetName()}",
+ "diffuse_color": [0.8, 0.8, 0.8],
+ "specular_color": [0.2, 0.2, 0.2],
+ "roughness": 0.5,
+ "metallic": 0.0,
+ "textures": {},
+ }
+ all_materials.append(material_info)
+ material_index = len(all_materials) - 1
+
+ faces = []
+ face_materials = []
+
+ idx = 0
+ for count in face_vertex_counts:
+ if count == 3:
+ face = [
+ face_vertex_indices[idx] + vertex_offset,
+ face_vertex_indices[idx + 1] + vertex_offset,
+ face_vertex_indices[idx + 2] + vertex_offset,
+ ]
+ faces.append(face)
+ face_materials.append(material_index)
+
+ elif count == 4:
+ face1 = [
+ face_vertex_indices[idx] + vertex_offset,
+ face_vertex_indices[idx + 1] + vertex_offset,
+ face_vertex_indices[idx + 2] + vertex_offset,
+ ]
+ face2 = [
+ face_vertex_indices[idx] + vertex_offset,
+ face_vertex_indices[idx + 2] + vertex_offset,
+ face_vertex_indices[idx + 3] + vertex_offset,
+ ]
+ faces.append(face1)
+ faces.append(face2)
+ face_materials.append(material_index)
+ face_materials.append(material_index)
+
+ idx += count
+
+ all_vertices.append(vertices)
+ all_faces.extend(faces)
+ all_face_materials.extend(face_materials)
+ all_uvs.extend(mesh_uvs) # EXTEND not append!
+
+ vertex_offset += len(vertices)
+ if has_uvs:
+ uv_offset += len(mesh_uvs) # Only increment when has_uvs
+
+ if not all_vertices or not all_faces:
+ print(f"Error: No valid meshes found in {usd_file_path}")
+ return None, None, None, None, None
+
+ merged_vertices = np.vstack(all_vertices)
+ merged_faces = np.array(all_faces, dtype=np.int32)
+
+ # Copy textures
+ if temp_dir:
+ for material in all_materials:
+ new_textures = {}
+ for tex_type, tex_path in material["textures"].items():
+ if os.path.exists(tex_path):
+ tex_name = os.path.basename(tex_path)
+ dest_path = os.path.join(temp_dir, tex_name)
+ shutil.copy2(tex_path, dest_path)
+ new_textures[tex_type] = dest_path
+ material["textures"] = new_textures
+
+ # Return flat UV list (not nested)
+ return merged_vertices, merged_faces, all_materials, all_face_materials, all_uvs
+
+ except Exception as e:
+ print(f"Error loading USD file: {str(e)}")
+
+ traceback.print_exc()
+ return None, None, None, None, None
+
+
+def save_obj_with_materials(
+ vertices, faces, materials, face_materials, obj_file_path, uvs=None
+):
+ """
+ Save vertices, faces and materials as OBJ and MTL files.
+ """
+ mtl_file_path = obj_file_path.replace(".obj", ".mtl")
+ mtl_filename = os.path.basename(mtl_file_path)
+
+ # Write MTL file
+ with open(mtl_file_path, "w") as f:
+ for i, material in enumerate(materials):
+ f.write(f"newmtl {material['name']}\n")
+ f.write(f"Ns 90.0\n")
+ f.write(f"Ka 1.0 1.0 1.0\n")
+
+ diffuse = material["diffuse_color"]
+ f.write(f"Kd {diffuse[0]} {diffuse[1]} {diffuse[2]}\n")
+
+ specular = material["specular_color"]
+ f.write(f"Ks {specular[0]} {specular[1]} {specular[2]}\n")
+
+ f.write(f"Ns {(1.0 - material['roughness']) * 100.0}\n")
+ f.write(f"d 1.0\n")
+ f.write(f"illum 2\n")
+
+ for tex_type, tex_path in material["textures"].items():
+ if not os.path.exists(tex_path):
+ continue
+
+ tex_rel_path = os.path.basename(tex_path)
+ if tex_type == "diffuse":
+ f.write(f"map_Kd {tex_rel_path}\n")
+ elif tex_type == "normal":
+ f.write(f"map_Bump {tex_rel_path}\n")
+ elif tex_type == "roughness":
+ f.write(f"map_Ns {tex_rel_path}\n")
+ elif tex_type == "metallic" or tex_type == "specular":
+ f.write(f"map_Ks {tex_rel_path}\n")
+ elif tex_type == "bump":
+ f.write(f"map_bump {tex_rel_path}\n")
+
+ f.write("\n")
+
+ # Write OBJ file
+ with open(obj_file_path, "w") as f:
+ f.write(f"mtllib {mtl_filename}\n\n")
+
+ # Write vertices
+ for v in vertices:
+ f.write(f"v {v[0]} {v[1]} {v[2]}\n")
+
+ f.write("\n")
+
+ # Write UVs
+ have_uvs = uvs is not None and len(uvs) > 0
+ if have_uvs:
+ for uv in uvs:
+ f.write(f"vt {uv[0]} {uv[1]}\n")
+ else:
+ for _ in range(len(vertices)):
+ f.write(f"vt 0.0 0.0\n")
+
+ f.write("\n")
+
+ # Write normals
+ for _ in range(len(vertices)):
+ f.write(f"vn 0.0 0.0 1.0\n")
+
+ f.write("\n")
+
+ # Group faces by material
+ material_to_faces = {}
+ for i, face in enumerate(faces):
+ mat_idx = face_materials[i]
+ if mat_idx not in material_to_faces:
+ material_to_faces[mat_idx] = []
+ material_to_faces[mat_idx].append((i, face))
+
+ # Write faces
+ for mat_idx, mat_faces in material_to_faces.items():
+ if mat_idx < len(materials):
+ material_name = materials[mat_idx]["name"]
+ f.write(f"usemtl {material_name}\n")
+
+ for face_idx, face in mat_faces:
+ f.write(
+ f"f {face[0]+1}/{face[0]+1}/{face[0]+1} {face[1]+1}/{face[1]+1}/{face[1]+1} {face[2]+1}/{face[2]+1}/{face[2]+1}\n"
+ )
+
+
+def convert_usd_to_obj(
+ usd_path: str,
+ output_obj_path: Optional[str] = None,
+) -> Tuple[str, str]:
+ """
+ Convert a USD file to OBJ format with materials.
+
+ Args:
+ usd_path: Path to input USD file
+ output_obj_path: Optional path for output OBJ (if None, creates temp file)
+
+ Returns:
+ Tuple of (obj_path, temp_dir)
+ temp_dir is the temporary directory created (or None if output_obj_path was specified)
+ """
+ # Create temp directory for OBJ and textures
+ if output_obj_path:
+ temp_dir = None
+ obj_path = output_obj_path
+ texture_dir = os.path.dirname(obj_path)
+ else:
+ temp_dir = tempfile.mkdtemp(prefix="vomp_usd_")
+ obj_path = os.path.join(temp_dir, "model.obj")
+ texture_dir = temp_dir
+
+ # Load USD and convert
+ vertices, faces, materials, face_materials, uvs = load_mesh_from_usd(
+ usd_path, temp_dir=texture_dir
+ )
+
+ if vertices is None:
+ raise ValueError(f"Failed to load USD file: {usd_path}")
+
+ # Save as OBJ with materials
+ save_obj_with_materials(
+ vertices, faces, materials, face_materials, obj_path, uvs=uvs
+ )
+
+ return obj_path, temp_dir
diff --git a/deps/vomp/vomp/inference/utils.py b/deps/vomp/vomp/inference/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..7388ae5d76ca36a72d9c2e11ac481022892cf0dc
--- /dev/null
+++ b/deps/vomp/vomp/inference/utils.py
@@ -0,0 +1,605 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Utility classes and functions for material property inference.
+
+This module provides supporting utilities including lazy model loading
+and material property upsampling capabilities.
+"""
+
+import os
+from typing import Optional, Union, Tuple, Dict, Any
+
+import torch
+import numpy as np
+import trimesh
+from torchvision import transforms
+from scipy.spatial import cKDTree
+
+
+class LazyLoadDino:
+ """
+ Lazy-loading DINO model wrapper with shared class-level storage.
+
+ Prevents multiple model instantiations and reduces memory overhead
+ by loading models only when accessed and sharing across instances.
+ """
+
+ __model__: Optional[torch.nn.Module] = None
+ __transform__: Optional[transforms.Compose] = None
+ __n_patch__: Optional[int] = None
+ __device__: Optional[torch.device] = None
+ __use_trt__: bool = False
+ __cuda_stream__: Optional[torch.cuda.Stream] = None
+
+ def __init__(
+ self,
+ model_name: str = "dinov2_vitl14_reg",
+ device: Union[str, torch.device, None] = None,
+ use_trt: bool = False,
+ ):
+ """
+ Initialize LazyLoadDino with model name and device.
+
+ Args:
+ model_name (str): Name of the DINO model to load
+ device (Union[str, torch.device, None]): Device to load the model on.
+ If None, defaults to "cuda" if available, else "cpu"
+ use_trt (bool): Whether to use TensorRT acceleration for inference (significantly faster).
+ Requires torch-tensorrt package to be installed.
+ """
+ self.model_name = model_name
+ self.use_trt = use_trt
+ if device is None:
+ self._device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+ else:
+ self._device = torch.device(device)
+
+ @classmethod
+ def _load_model_and_transform(
+ cls, model_name: str, device: torch.device, use_trt: bool = False
+ ) -> None:
+ """Load the DINO model and setup transforms if not already loaded"""
+ if cls.__model__ is None:
+ print(f"Loading DINO model: {model_name} on device: {device}...")
+ cls.__model__ = torch.hub.load("facebookresearch/dinov2", model_name)
+ cls.__model__.eval().to(device)
+ cls.__device__ = device
+ cls.__use_trt__ = use_trt
+
+ cls.__transform__ = transforms.Compose(
+ [
+ transforms.Normalize(
+ mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
+ ),
+ ]
+ )
+
+ cls.__n_patch__ = 518 // 14
+
+ print(f"DINO model loaded successfully. Patch size: {cls.__n_patch__}")
+
+ # Apply TensorRT compilation if requested
+ if use_trt:
+ cls._compile_with_tensorrt()
+
+ @classmethod
+ def _compile_with_tensorrt(cls) -> None:
+ """Compile the loaded DINO model with TensorRT acceleration."""
+ try:
+ import torch_tensorrt
+ except ImportError:
+ print(
+ "\nโ Error: torch-tensorrt package is required for TensorRT acceleration."
+ )
+ print("Please install it using:")
+ print(
+ "pip install -U torch-tensorrt --no-deps --index-url https://download.pytorch.org/whl/cu118"
+ )
+ print("\nExiting...")
+ exit(1)
+
+ print("Compiling DINO model with TensorRT...")
+ try:
+ original_model_name = "dinov2_vitl14_reg"
+
+ # Create a non-default CUDA stream to avoid synchronization warnings
+ if cls.__device__.type == "cuda":
+ cls.__cuda_stream__ = torch.cuda.Stream()
+
+ # Create sample inputs with different batch sizes that will be used
+ sample_inputs = [
+ torch.randn(1, 3, 518, 518).to(cls.__device__), # Single image
+ torch.randn(4, 3, 518, 518).to(cls.__device__),
+ torch.randn(8, 3, 518, 518).to(cls.__device__),
+ ]
+
+ # Compile with TensorRT
+ print("Compiling with TensorRT backend...")
+ cls.__model__ = torch.compile(cls.__model__, backend="tensorrt")
+
+ print("Warming up with different batch sizes...")
+ with torch.inference_mode():
+ for i, sample_input in enumerate(sample_inputs):
+ print(f" Warming up batch size {sample_input.shape[0]}...")
+ try:
+ _ = cls.__model__(sample_input)
+ print(
+ f" โ Batch size {sample_input.shape[0]} compiled successfully"
+ )
+ except Exception as e:
+ print(f" โ Batch size {sample_input.shape[0]} failed: {e}")
+ continue
+
+ print("โ DINO model compiled with TensorRT successfully!")
+
+ except Exception as e:
+ print(f"โ TensorRT compilation failed: {e}")
+ print("Try to load without `use_trt`...")
+
+ def get_model(self) -> torch.nn.Module:
+ """
+ Get the loaded DINO model, loading it if necessary.
+
+ Returns:
+ torch.nn.Module: The loaded DINO model
+ """
+ self._load_model_and_transform(self.model_name, self._device, self.use_trt)
+ return self.__model__
+
+ def get_transform(self) -> transforms.Compose:
+ """
+ Get the image transform, loading the model if necessary.
+
+ Returns:
+ torchvision.transforms.Compose: The image transform pipeline
+ """
+ self._load_model_and_transform(self.model_name, self._device, self.use_trt)
+ return self.__transform__
+
+ @property
+ def n_patch(self) -> int:
+ """
+ Get the patch size, loading the model if necessary.
+
+ Returns:
+ int: Number of patches per side
+ """
+ self._load_model_and_transform(self.model_name, self._device, self.use_trt)
+ return self.__n_patch__
+
+ @property
+ def is_loaded(self) -> bool:
+ """Check if the model is currently loaded"""
+ return self.__model__ is not None
+
+ @property
+ def device(self) -> torch.device:
+ """Get the device the model will be/is loaded on"""
+ return self._device
+
+ @classmethod
+ def clear_model(cls) -> None:
+ """Clear the loaded model to free memory"""
+ if cls.__model__ is not None:
+ del cls.__model__
+ cls.__model__ = None
+ cls.__transform__ = None
+ cls.__n_patch__ = None
+ cls.__device__ = None
+ cls.__use_trt__ = False
+ cls.__cuda_stream__ = None
+
+ if torch.cuda.is_available():
+ torch.cuda.empty_cache()
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
+ torch.mps.empty_cache()
+ print("DINO model cleared from memory")
+
+
+class MaterialUpsampler:
+ """
+ Nearest-neighbor material property interpolation for spatial upsampling.
+
+ Efficiently interpolates material properties from sparse voxel data to arbitrary
+ 3D query points using scipy's cKDTree for fast spatial lookups.
+ """
+
+ def __init__(self, voxel_coords: np.ndarray, voxel_materials: np.ndarray):
+ """
+ Initialize the upsampler with voxel data.
+
+ Args:
+ voxel_coords: Voxel coordinates (N, 3) in world space
+ voxel_materials: Material properties per voxel (N, 3) [E, nu, rho]
+ """
+ self.voxel_coords = np.asarray(voxel_coords, dtype=np.float32)
+ self.voxel_materials = np.asarray(voxel_materials, dtype=np.float32)
+
+ # Build KDTree for fast nearest neighbor search
+
+ self.tree = cKDTree(self.voxel_coords)
+
+ def interpolate(
+ self, query_points: np.ndarray, k: int = 1
+ ) -> Tuple[np.ndarray, np.ndarray]:
+ """
+ Interpolate material properties to query points via k-nearest neighbors.
+
+ Args:
+ query_points: Target coordinates (M, 3) for interpolation
+ k: Number of neighbors for interpolation (1=NN, >1=weighted average)
+
+ Returns:
+ Tuple of (interpolated_materials, distances):
+ - interpolated_materials: (M, 3) [E, nu, rho] at query points
+ - distances: (M,) distance to nearest neighbor
+
+ Raises:
+ ValueError: If query_points shape is not (N, 3)
+ """
+ query_points = np.asarray(query_points, dtype=np.float32)
+
+ if query_points.shape[1] != 3:
+ raise ValueError(f"Query points must be (N, 3), got {query_points.shape}")
+
+ # Find nearest neighbors
+ distances, indices = self.tree.query(query_points, k=k)
+
+ if k == 1:
+ # Simple nearest neighbor
+ interpolated_materials = self.voxel_materials[indices]
+ else:
+ # Distance-weighted average of k nearest neighbors
+ weights = 1.0 / (
+ distances + 1e-10
+ ) # Add small epsilon to avoid division by zero
+ weights = weights / weights.sum(axis=1, keepdims=True) # Normalize weights
+
+ # Weighted average of materials
+ interpolated_materials = np.sum(
+ self.voxel_materials[indices] * weights[:, :, np.newaxis], axis=1
+ )
+ distances = distances[:, 0] # Return distance to closest neighbor
+
+ return interpolated_materials, distances
+
+ def interpolate_to_gaussians(
+ self, gaussian_model: "Gaussian"
+ ) -> Tuple[np.ndarray, np.ndarray]:
+ """
+ Convenience method to interpolate to Gaussian splat centers.
+
+ Args:
+ gaussian_model: Gaussian splat model
+
+ Returns:
+ Tuple of (interpolated_materials, distances)
+ """
+ # Get Gaussian positions
+ gaussian_positions = gaussian_model.get_xyz.detach().cpu().numpy()
+
+ return self.interpolate(gaussian_positions)
+
+ def save_results(
+ self,
+ query_points: np.ndarray,
+ materials: np.ndarray,
+ distances: np.ndarray,
+ output_path: str,
+ format: str = "npz",
+ ):
+ """
+ Save interpolation results to file.
+
+ Args:
+ query_points: Query coordinates (M, 3)
+ materials: Interpolated materials (M, 3)
+ distances: Distances to nearest neighbors (M,)
+ output_path: Output file path
+ format: File format ("npz" or "pth")
+ """
+
+ os.makedirs(
+ os.path.dirname(output_path) if os.path.dirname(output_path) else ".",
+ exist_ok=True,
+ )
+
+ if format.lower() == "npz":
+ # Create structured array
+ num_points = len(query_points)
+ dtype = [
+ ("x", " int:
+ """Get number of voxels"""
+ return len(self.voxel_coords)
+
+ @property
+ def material_stats(self) -> Dict[str, Any]:
+ """Get material property statistics"""
+ return {
+ "youngs_modulus": {
+ "mean": self.voxel_materials[:, 0].mean(),
+ "std": self.voxel_materials[:, 0].std(),
+ },
+ "poisson_ratio": {
+ "mean": self.voxel_materials[:, 1].mean(),
+ "std": self.voxel_materials[:, 1].std(),
+ },
+ "density": {
+ "mean": self.voxel_materials[:, 2].mean(),
+ "std": self.voxel_materials[:, 2].std(),
+ },
+ }
+
+
+def save_materials(
+ materials_dict: Dict[str, np.ndarray],
+ output_path: str,
+ format: str = "npz",
+) -> None:
+ """
+ Save materials to file in specified format.
+
+ This utility function saves material property dictionaries to disk in various formats.
+ It handles coordinate detection automatically and creates appropriate structured arrays.
+
+ Args:
+ materials_dict: Dictionary containing material properties with keys:
+ - 'youngs_modulus': Young's modulus values
+ - 'poisson_ratio': Poisson's ratio values
+ - 'density': Density values
+ - Coordinates: Either 'voxel_coords_world' or 'splat_coords_world'
+ output_path: Output file path
+ format: File format ("npz" or "pth")
+
+ Raises:
+ ValueError: If no material data found or unsupported format
+
+ Examples:
+ >>> from vomp.inference.utils import save_materials
+ >>> save_materials(results, "materials.npz")
+ >>> save_materials(results, "materials.pth", format="pth")
+ """
+ os.makedirs(
+ os.path.dirname(output_path) if os.path.dirname(output_path) else ".",
+ exist_ok=True,
+ )
+
+ if format.lower() == "npz":
+ # Save as compressed numpy archive
+ if "voxel_data" in materials_dict:
+ # Already in structured array format
+ np.savez_compressed(output_path, **materials_dict)
+ else:
+ # Convert to structured array format
+ num_points = len(materials_dict.get("youngs_modulus", []))
+ if num_points == 0:
+ raise ValueError("No material data to save")
+
+ # Create structured array identical to existing pipeline
+ dtype = [
+ ("x", " Dict[str, np.ndarray]:
+ """
+ Load materials from file.
+
+ Args:
+ file_path: Path to materials file (.npz or .pth)
+
+ Returns:
+ Dictionary containing material properties
+ """
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"Materials file not found: {file_path}")
+
+ if file_path.endswith(".npz"):
+ data = np.load(file_path)
+ if "voxel_data" in data:
+ # Structured array format
+ voxel_data = data["voxel_data"]
+ return {
+ "x": voxel_data["x"],
+ "y": voxel_data["y"],
+ "z": voxel_data["z"],
+ "youngs_modulus": voxel_data["youngs_modulus"],
+ "poisson_ratio": voxel_data["poissons_ratio"],
+ "density": voxel_data["density"],
+ "coords_world": np.column_stack(
+ [voxel_data["x"], voxel_data["y"], voxel_data["z"]]
+ ),
+ }
+ else:
+ # Direct dictionary format
+ return dict(data)
+
+ elif file_path.endswith(".pth"):
+ # PyTorch format
+ data = torch.load(file_path, map_location="cpu")
+ return {
+ k: v.numpy() if isinstance(v, torch.Tensor) else v for k, v in data.items()
+ }
+
+ else:
+ raise ValueError(
+ f"Unsupported file format. Expected .npz or .pth, got: {file_path}"
+ )
+
+
+def get_mesh_transform_params(mesh_path: str) -> Tuple[np.ndarray, float]:
+ """
+ Compute normalization parameters for a mesh.
+
+ Computes the center and scale used to normalize a mesh to [-0.5, 0.5] range.
+ This matches the normalization used in mesh voxelization.
+
+ Args:
+ mesh_path: Path to mesh file (supports OBJ, PLY, STL, etc.)
+
+ Returns:
+ Tuple of (center, scale) where:
+ - center: 3D center point of the mesh bounding box (np.ndarray)
+ - scale: Maximum dimension of the bounding box (float)
+ """
+ mesh = trimesh.load(mesh_path)
+ if hasattr(mesh, "vertices"):
+ mesh_vertices = mesh.vertices
+ else:
+ vertices_list = []
+ for geometry in mesh.geometry.values():
+ if hasattr(geometry, "vertices"):
+ vertices_list.append(geometry.vertices)
+ if not vertices_list:
+ raise ValueError("No vertices found in mesh")
+ mesh_vertices = np.vstack(vertices_list)
+
+ # Compute transformation parameters
+ center = (mesh_vertices.min(axis=0) + mesh_vertices.max(axis=0)) / 2
+ scale = (mesh_vertices.max(axis=0) - mesh_vertices.min(axis=0)).max()
+
+ return center, scale
+
+
+def normalize_coords(
+ coords: np.ndarray, center: np.ndarray, scale: float, clip: bool = True
+) -> np.ndarray:
+ """
+ Normalize coordinates to [-0.5, 0.5] range.
+
+ Args:
+ coords: Input coordinates (N, 3)
+ center: Center point for normalization (3,)
+ scale: Scale factor for normalization (scalar)
+ clip: Whether to clip to [-0.5 + eps, 0.5 - eps] (default: True)
+
+ Returns:
+ Normalized coordinates (N, 3)
+ """
+ normalized = (coords - center) / scale
+ if clip:
+ normalized = np.clip(normalized, -0.5 + 1e-6, 0.5 - 1e-6)
+ return normalized
+
+
+def denormalize_coords(
+ coords: np.ndarray, center: np.ndarray, scale: float
+) -> np.ndarray:
+ """
+ Denormalize coordinates from [-0.5, 0.5] range back to original scale.
+
+ Args:
+ coords: Normalized coordinates (N, 3)
+ center: Center point used for normalization (3,)
+ scale: Scale factor used for normalization (scalar)
+
+ Returns:
+ Denormalized coordinates (N, 3) in original scale
+ """
+ return coords * scale + center
diff --git a/deps/vomp/vomp/inference/vomp.py b/deps/vomp/vomp/inference/vomp.py
new file mode 100644
index 0000000000000000000000000000000000000000..899c832a4147b36aae2d6b9687293ed0e73946ec
--- /dev/null
+++ b/deps/vomp/vomp/inference/vomp.py
@@ -0,0 +1,3600 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Main Vomp model class for material property inference.
+
+This module contains the core Vomp model that combines geometry encoding,
+feature extraction, and material property prediction for 3D objects.
+"""
+
+import os
+import json
+import glob
+import math
+from typing import Dict, Any, Optional, Union, Tuple, List, Protocol
+from subprocess import DEVNULL, call
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import numpy as np
+import trimesh
+from PIL import Image
+from safetensors.torch import load_file
+from easydict import EasyDict as edict
+from scipy.spatial.distance import cdist
+from vomp.inference.ply_utils import write_ply_vertices, read_ply_vertices
+import utils3d
+from diff_gaussian_rasterization import (
+ GaussianRasterizationSettings,
+ GaussianRasterizer,
+)
+
+import kaolin.ops.conversions.gaussians as gs_ops
+
+from vomp.models.geometry_encoder import (
+ ElasticGeometryEncoder,
+ ElasticSLatVoxelDecoder,
+)
+from vomp.models.material_vae.beta_tc import TripletVAE
+from vomp.utils.data_utils import recursive_to_device
+from vomp.modules.sparse import SparseTensor
+from vomp.utils.material_transforms import MaterialPropertyTransform
+from vomp.utils.render_utils import yaw_pitch_r_fov_to_extrinsics_intrinsics
+from vomp.inference.utils import (
+ LazyLoadDino,
+ MaterialUpsampler,
+ get_mesh_transform_params,
+ denormalize_coords,
+)
+from vomp.representations.gaussian import Gaussian
+from vomp.inference.replicator_renderer import render_with_replicator
+from vomp.inference.usd_utils import convert_usd_to_obj
+import tempfile
+import shutil
+import multiprocessing
+from functools import partial
+from subprocess import PIPE, Popen
+
+
+class RenderFunction(Protocol):
+ """Protocol for custom rendering functions."""
+
+ def __call__(
+ self,
+ obj_3d: Any,
+ output_dir: str,
+ num_views: int,
+ image_size: int,
+ **kwargs: Any,
+ ) -> List[Dict[str, Any]]:
+ """Render object and return frame metadata."""
+ ...
+
+
+class VoxelizeFunction(Protocol):
+ """Protocol for custom voxelization functions."""
+
+ def __call__(self, obj_3d: Any, output_dir: str, **kwargs: Any) -> np.ndarray:
+ """Voxelize object and return voxel centers as (N, 3) array."""
+ ...
+
+
+class Vomp(nn.Module):
+ """
+ End-to-end material property inference model for 3D objects.
+
+ Vomp combines geometry encoding, visual feature extraction via DINO,
+ and material VAE to predict mechanical properties (Young's modulus, Poisson's
+ ratio, density) from 3D representations including Gaussian splats and meshes.
+
+ The model processes 3D objects through the following pipeline:
+ 1. Multi-view rendering from sampled camera positions
+ 2. Voxelization of the 3D geometry
+ 3. DINO feature extraction from rendered views
+ 4. Geometry encoding with transformer architecture
+ 5. Material property prediction via VAE decoder
+
+ Supports both high-level APIs (get_splat_materials, get_mesh_materials) and
+ low-level APIs (get_features, predict_materials) for custom workflows.
+
+ Examples:
+ >>> # High-level API for Gaussian splats
+ >>> model = Vomp.from_checkpoint(config_path, geo_dir, mat_dir, norm_path)
+ >>> results = model.get_splat_materials("gaussian.ply")
+ >>>
+ >>> # High-level API for meshes
+ >>> results = model.get_mesh_materials("mesh.obj")
+ >>>
+ >>> # Custom workflow
+ >>> coords, features = model.get_features(obj, render_func, voxel_func)
+ >>> results = model.predict_materials(coords, features)
+ """
+
+ def __init__(
+ self,
+ geometry_encoder: ElasticGeometryEncoder,
+ matvae: TripletVAE,
+ material_transform: MaterialPropertyTransform,
+ decoder: Optional[ElasticSLatVoxelDecoder] = None,
+ config: Optional[Dict[str, Any]] = None,
+ use_trt: bool = False,
+ ):
+ """
+ Initialize Vomp model.
+
+ Sets up the complete material property inference pipeline by combining
+ a geometry encoder, material VAE, and property transforms. All models
+ are automatically set to evaluation mode and MatVAE weights are frozen
+ to preserve pre-trained material knowledge.
+
+ Args:
+ geometry_encoder: Pre-trained geometry transformer for encoding 3D structure
+ into latent representations from sparse voxel features
+ matvae: Pre-trained material VAE for decoding material properties from
+ latent codes (Young's modulus, Poisson's ratio, density)
+ material_transform: Transform handler for normalizing/denormalizing
+ material properties between physical units and model space
+ decoder: Optional voxel decoder for encoder-decoder training modes.
+ Required when config['training_mode'] is 'encoder_decoder_*'
+ config: Model configuration dictionary containing training mode,
+ normalization parameters, and other model settings
+ use_trt: Whether to use TensorRT acceleration for DINO model inference (significantly faster).
+ Requires torch-tensorrt package to be installed.
+
+ Note:
+ - All models are automatically moved to evaluation mode
+ - MatVAE parameters are frozen to preserve pre-trained weights
+ - The geometry encoder remains trainable for fine-tuning
+ """
+ super().__init__()
+
+ self.geometry_encoder = geometry_encoder
+ self.matvae = matvae
+ self.decoder = decoder
+ self.material_transform = material_transform
+ self.config = config or {}
+ self.use_trt = use_trt
+
+ # Configure models for inference
+ self.geometry_encoder.eval()
+ self.matvae.eval()
+ if self.decoder is not None:
+ self.decoder.eval()
+
+ # Freeze pre-trained MatVAE weights
+ for param in self.matvae.parameters():
+ param.requires_grad = False
+
+ @classmethod
+ def from_checkpoint(
+ cls,
+ config_path: Optional[str] = None,
+ geometry_checkpoint_dir: Optional[str] = None,
+ matvae_checkpoint_dir: Optional[str] = None,
+ normalization_params_path: Optional[str] = None,
+ geometry_ckpt: str = "latest",
+ device: Union[str, torch.device, None] = None,
+ use_trt: bool = False,
+ ) -> "Vomp":
+ """
+ Load Vomp model from pretrained checkpoints.
+
+ This method loads all required components including the geometry encoder,
+ material VAE, and normalization parameters from their respective checkpoint
+ directories or direct file paths and combines them into a ready-to-use inference model.
+
+ Args:
+ config_path: Path to configuration JSON file. Can be either:
+ - Training config (contains models, dataset, trainer sections)
+ - Inference config (contains checkpoint paths + models, dataset sections)
+ If inference config is provided, checkpoint paths are automatically loaded.
+ geometry_checkpoint_dir: Directory containing geometry encoder checkpoints
+ (looks for files like "geometry_encoder_step*.pt") OR direct path to
+ geometry encoder .pt file. If None and config_path is inference config,
+ loaded from config.
+ matvae_checkpoint_dir: Directory containing MatVAE checkpoints
+ (looks for "checkpoints/checkpoint_*/model.safetensors") OR direct path to
+ MatVAE .safetensors file. If None and config_path is inference config,
+ loaded from config.
+ normalization_params_path: Path to JSON file with material property
+ normalization parameters. If None and config_path is inference config,
+ loaded from config.
+ geometry_ckpt: Geometry checkpoint to load. Only used when geometry_checkpoint_dir
+ is a directory. Can be:
+ - "latest": Load most recent checkpoint by step number
+ - Step number (str): Load specific step checkpoint
+ device: Device to load models on. If None, uses CUDA if available,
+ otherwise CPU
+ use_trt: Whether to use TensorRT acceleration for DINO model inference (significantly faster).
+ Requires torch-tensorrt package to be installed.
+
+ Returns:
+ Fully loaded Vomp model ready for inference
+
+ Raises:
+ FileNotFoundError: If required checkpoint files are not found
+ ValueError: If checkpoint format is invalid or required arguments not provided
+
+ Examples:
+ # Using inference config (recommended)
+ >>> model = Vomp.from_checkpoint(
+ ... config_path="configs/materials/inference.json"
+ ... )
+
+ # Using inference config with overrides
+ >>> model = Vomp.from_checkpoint(
+ ... config_path="configs/materials/inference.json",
+ ... geometry_checkpoint_dir="custom/path/to/geometry_transformer.pt"
+ ... )
+
+ # Using training config with direct file paths
+ >>> model = Vomp.from_checkpoint(
+ ... config_path="configs/materials/geometry_encoder/train.json",
+ ... geometry_checkpoint_dir="weights/geometry_transformer.pt",
+ ... matvae_checkpoint_dir="weights/matvae.safetensors",
+ ... normalization_params_path="weights/normalization_params.json"
+ ... )
+
+ # Using training config with directories (advanced)
+ >>> model = Vomp.from_checkpoint(
+ ... config_path="configs/materials/geometry_encoder/train.json",
+ ... geometry_checkpoint_dir="outputs/fixes/normal",
+ ... matvae_checkpoint_dir="outputs/matvae2",
+ ... normalization_params_path="outputs/matvae2/normalization_params.json",
+ ... geometry_ckpt="latest"
+ ... )
+ """
+ if device is None:
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+ else:
+ device = torch.device(device)
+
+ print(f"Loading Vomp model on device: {device}")
+
+ # Validate config_path is provided
+ if config_path is None:
+ raise ValueError("config_path must be provided")
+
+ if not os.path.exists(config_path):
+ raise FileNotFoundError(f"Config file not found: {config_path}")
+
+ # Load configuration
+ with open(config_path, "r") as f:
+ config = json.load(f)
+
+ # Check if this is an inference config (has checkpoint paths) or training config
+ is_inference_config = any(
+ key in config
+ for key in [
+ "geometry_checkpoint_dir",
+ "matvae_checkpoint_dir",
+ "normalization_params_path",
+ ]
+ )
+
+ if is_inference_config:
+ print(f"โ Detected inference config: {config_path}")
+ # Use inference config values as defaults if individual arguments not provided
+ if geometry_checkpoint_dir is None:
+ geometry_checkpoint_dir = config.get("geometry_checkpoint_dir")
+ if matvae_checkpoint_dir is None:
+ matvae_checkpoint_dir = config.get("matvae_checkpoint_dir")
+ if normalization_params_path is None:
+ normalization_params_path = config.get("normalization_params_path")
+ else:
+ print(f"โ Detected training config: {config_path}")
+
+ # Validate that we have all required parameters
+ if geometry_checkpoint_dir is None:
+ raise ValueError(
+ "geometry_checkpoint_dir must be provided either directly or via inference config"
+ )
+ if matvae_checkpoint_dir is None:
+ raise ValueError(
+ "matvae_checkpoint_dir must be provided either directly or via inference config"
+ )
+ if normalization_params_path is None:
+ raise ValueError(
+ "normalization_params_path must be provided either directly or via inference config"
+ )
+
+ cfg = edict(config)
+ cfg.load_dir = geometry_checkpoint_dir
+ cfg.ckpt = geometry_ckpt
+ cfg.matvae_dir = matvae_checkpoint_dir
+
+ # Find geometry checkpoint
+ cfg = cls._find_ckpt(cfg)
+
+ # Find MatVAE checkpoint
+ cfg = cls._find_matvae_ckpt(cfg)
+
+ # Load material transform
+ normalization_type = cfg.dataset.get("normalization_type", "log_minmax")
+ material_transform = cls._load_material_transform(
+ normalization_params_path, normalization_type
+ )
+
+ # Load models
+ geometry_encoder, decoder = cls._load_geometry_models(cfg, device)
+ matvae = cls._load_matvae(cfg, device)
+
+ print("โ All models loaded successfully")
+
+ # Create instance
+ instance = cls(
+ geometry_encoder=geometry_encoder,
+ matvae=matvae,
+ material_transform=material_transform,
+ decoder=decoder,
+ config=cfg,
+ use_trt=use_trt,
+ )
+
+ # Warmup DINO model with TensorRT
+ if use_trt:
+ print("Warming up DINO model with TensorRT...")
+ dino = LazyLoadDino(
+ model_name="dinov2_vitl14_reg", device=device, use_trt=True
+ )
+ # Trigger model loading and TensorRT compilation
+ _ = dino.get_model()
+ print("โ DINO model warmed up with TensorRT")
+
+ return instance
+
+ @staticmethod
+ def _find_ckpt(cfg: edict) -> edict:
+ """
+ Locate and validate geometry encoder checkpoint file.
+
+ Determines the appropriate geometry encoder checkpoint to load based on
+ configuration. Supports both direct file paths and directory-based
+ checkpoint discovery with step-based selection.
+
+ Args:
+ cfg: Configuration object containing:
+ - load_dir: Checkpoint directory or direct file path
+ - ckpt: Checkpoint selection ('latest', 'none', or step number)
+
+ Returns:
+ Updated configuration with checkpoint information:
+ - load_ckpt: Step number to load (None if no checkpoint)
+ - geometry_checkpoint_file: Direct path if file provided
+
+ Raises:
+ FileNotFoundError: If required checkpoint files are not found
+
+ Note:
+ - If load_dir is a file path, uses it directly
+ - If load_dir is a directory, searches for 'misc_step*.pt' files
+ - 'latest' selects highest step number checkpoint
+ - 'none' disables checkpoint loading
+ """
+ cfg["load_ckpt"] = None
+ cfg["geometry_checkpoint_file"] = None
+ searched_paths = []
+
+ if cfg.load_dir != "":
+ # Check if load_dir is a direct file path
+ if os.path.isfile(cfg.load_dir):
+ # Direct file path provided
+ cfg.geometry_checkpoint_file = cfg.load_dir
+ print(f"โ Using direct geometry checkpoint: {cfg.load_dir}")
+ else:
+ # Directory path provided - use original logic
+ if cfg.ckpt == "latest":
+ search_pattern = os.path.join(
+ cfg.load_dir, "ckpts", "misc_step*.pt"
+ )
+ searched_paths.append(search_pattern)
+ files = glob.glob(search_pattern)
+ if len(files) != 0:
+ cfg.load_ckpt = max(
+ [
+ int(os.path.basename(f).split("step")[-1].split(".")[0])
+ for f in files
+ ]
+ )
+ else:
+ raise FileNotFoundError(
+ f"No geometry encoder checkpoint files found.\n"
+ f"Searched for pattern: {search_pattern}\n"
+ f"Directory contents: {os.listdir(os.path.join(cfg.load_dir, 'ckpts')) if os.path.exists(os.path.join(cfg.load_dir, 'ckpts')) else 'Directory does not exist'}\n"
+ f"Expected filename pattern: misc_step*.pt"
+ )
+ elif cfg.ckpt == "none":
+ cfg.load_ckpt = None
+ else:
+ cfg.load_ckpt = int(cfg.ckpt)
+ # Validate that the specific checkpoint exists
+ expected_path = os.path.join(
+ cfg.load_dir, "ckpts", f"misc_step{cfg.load_ckpt:07d}.pt"
+ )
+ searched_paths.append(expected_path)
+ if not os.path.exists(expected_path):
+ raise FileNotFoundError(
+ f"Specified geometry encoder checkpoint not found.\n"
+ f"Searched for: {expected_path}\n"
+ f"Directory contents: {os.listdir(os.path.join(cfg.load_dir, 'ckpts')) if os.path.exists(os.path.join(cfg.load_dir, 'ckpts')) else 'Directory does not exist'}"
+ )
+ else:
+ raise ValueError("geometry_checkpoint_dir cannot be empty")
+
+ return cfg
+
+ @staticmethod
+ def _find_matvae_ckpt(cfg: edict) -> edict:
+ """
+ Locate and validate MatVAE checkpoint file.
+
+ Searches for MatVAE model checkpoints in either direct file paths or
+ structured checkpoint directories. Automatically selects the latest
+ checkpoint when multiple versions are available.
+
+ Args:
+ cfg: Configuration object containing:
+ - matvae_dir: MatVAE checkpoint directory or direct .safetensors path
+
+ Returns:
+ Updated configuration with:
+ - matvae_checkpoint: Path to selected checkpoint file
+
+ Raises:
+ FileNotFoundError: If no valid MatVAE checkpoint is found
+
+ Note:
+ Checkpoint search priority:
+ 1. Direct .safetensors file path
+ 2. {matvae_dir}/checkpoints/checkpoint_*/model.safetensors (latest by number)
+ 3. {matvae_dir}/model.safetensors (fallback)
+ """
+ cfg["matvae_checkpoint"] = None
+ searched_paths = []
+
+ if cfg.matvae_dir == "":
+ raise ValueError("matvae_checkpoint_dir cannot be empty")
+
+ # Check if matvae_dir is a direct file path
+ if os.path.isfile(cfg.matvae_dir):
+ # Direct file path provided
+ cfg.matvae_checkpoint = cfg.matvae_dir
+ print(f"โ Using direct MatVAE checkpoint: {cfg.matvae_dir}")
+ else:
+ # Directory path provided - use original logic
+ # Look for model.safetensors in checkpoints directory
+ # Pattern: {matvae_dir}/checkpoints/checkpoint_*/model.safetensors
+ checkpoint_pattern = os.path.join(
+ cfg.matvae_dir, "checkpoints", "checkpoint_*", "model.safetensors"
+ )
+ searched_paths.append(checkpoint_pattern)
+ files = glob.glob(checkpoint_pattern)
+
+ if len(files) > 0:
+ # Find the latest checkpoint by extracting checkpoint number
+ def get_checkpoint_num(path):
+ # Extract number from checkpoint_XXX directory name
+ checkpoint_dir = os.path.basename(os.path.dirname(path))
+ if checkpoint_dir.startswith("checkpoint_"):
+ try:
+ return int(checkpoint_dir.split("_")[1])
+ except (IndexError, ValueError):
+ return 0
+ return 0
+
+ # Use the checkpoint with the highest number
+ latest_file = max(files, key=get_checkpoint_num)
+ cfg.matvae_checkpoint = latest_file
+ print(f"โ Found MatVAE checkpoint: {latest_file}")
+ else:
+ # Also try looking for model.safetensors directly in the directory
+ direct_path = os.path.join(cfg.matvae_dir, "model.safetensors")
+ searched_paths.append(direct_path)
+ if os.path.exists(direct_path):
+ cfg.matvae_checkpoint = direct_path
+ print(f"โ Found MatVAE checkpoint: {direct_path}")
+ else:
+ # Gather directory information for error message
+ dir_contents = "Directory does not exist"
+ checkpoints_dir = os.path.join(cfg.matvae_dir, "checkpoints")
+ if os.path.exists(cfg.matvae_dir):
+ dir_contents = (
+ f"Directory contents: {os.listdir(cfg.matvae_dir)}"
+ )
+ if os.path.exists(checkpoints_dir):
+ checkpoint_subdirs = [
+ d
+ for d in os.listdir(checkpoints_dir)
+ if os.path.isdir(os.path.join(checkpoints_dir, d))
+ ]
+ dir_contents += (
+ f"\nCheckpoints subdirectories: {checkpoint_subdirs}"
+ )
+
+ raise FileNotFoundError(
+ f"No MatVAE checkpoint found.\n"
+ f"Searched for patterns:\n"
+ f" 1. {checkpoint_pattern}\n"
+ f" 2. {direct_path}\n"
+ f"{dir_contents}\n"
+ f"Expected:\n"
+ f" - model.safetensors in checkpoints/checkpoint_*/ subdirectories, or\n"
+ f" - model.safetensors directly in {cfg.matvae_dir}"
+ )
+ return cfg
+
+ @staticmethod
+ def _load_material_transform(
+ normalization_params_file: str, normalization_type: str = "log_minmax"
+ ) -> MaterialPropertyTransform:
+ """
+ Load and configure material property normalization transform.
+
+ Creates a MaterialPropertyTransform instance with normalization parameters
+ loaded from a JSON file. Supports log-min-max normalization for converting
+ between physical units and normalized model space.
+
+ Args:
+ normalization_params_file: Path to JSON file containing normalization
+ parameters with keys: E_min, E_max, nu_min, nu_max, rho_min, rho_max
+ normalization_type: Type of normalization to apply. Currently supports:
+ - 'log_minmax': Log transform for E and rho, linear for nu
+
+ Returns:
+ Configured MaterialPropertyTransform ready for use
+
+ Note:
+ - Young's modulus (E) and density (rho) use log10 transformation
+ - Poisson's ratio (nu) uses linear normalization
+ - All parameters are normalized to [0,1] range for model input
+
+ Raises:
+ FileNotFoundError: If normalization_params_file doesn't exist
+ KeyError: If required parameters are missing from the file
+ """
+ with open(normalization_params_file, "r") as f:
+ params = json.load(f)
+
+ transform = MaterialPropertyTransform(normalization_type=normalization_type)
+
+ if normalization_type == "log_minmax":
+ transform.E_min = math.log10(params["E_min"])
+ transform.E_max = math.log10(params["E_max"])
+ transform.nu_min = params["nu_min"]
+ transform.nu_max = params["nu_max"]
+ transform.rho_min = math.log10(params["rho_min"])
+ transform.rho_max = math.log10(params["rho_max"])
+ transform._stats_computed = True
+
+ return transform
+
+ @staticmethod
+ def _load_geometry_models(
+ cfg: edict, device: torch.device
+ ) -> Tuple[ElasticGeometryEncoder, Optional[ElasticSLatVoxelDecoder]]:
+ """
+ Initialize and load geometry encoder and optional decoder models.
+
+ Creates model instances from configuration and loads pre-trained weights
+ from checkpoint files. Supports both encoder-only and encoder-decoder
+ architectures based on configuration.
+
+ Args:
+ cfg: Configuration object containing:
+ - models.geometry_encoder.args: Encoder model arguments
+ - models.decoder.args: Decoder model arguments (optional)
+ - geometry_checkpoint_file: Direct path to checkpoint (if provided)
+ - load_dir: Checkpoint directory (if using directory structure)
+ - load_ckpt: Step number for checkpoint loading
+ device: Target device for model placement
+
+ Returns:
+ Tuple of (geometry_encoder, decoder):
+ - geometry_encoder: Loaded ElasticGeometryEncoder instance
+ - decoder: ElasticSLatVoxelDecoder instance or None if not configured
+
+ Raises:
+ FileNotFoundError: If checkpoint files don't exist
+ RuntimeError: If model state dict loading fails
+
+ Note:
+ - Models are automatically moved to specified device
+ - Checkpoint loading is strict (all parameters must match)
+ - Decoder is only loaded if specified in configuration
+ - Supports both direct file paths and directory-based checkpoints
+ """
+
+ # Load geometry encoder
+ try:
+ geometry_encoder = ElasticGeometryEncoder(
+ **cfg.models.geometry_encoder.args
+ ).to(device)
+ except Exception as e:
+ raise RuntimeError(f"Failed to initialize geometry encoder model: {e}")
+
+ # Load decoder if specified
+ decoder = None
+ if "decoder" in cfg.models:
+ try:
+ decoder = ElasticSLatVoxelDecoder(**cfg.models.decoder.args).to(device)
+ except Exception as e:
+ raise RuntimeError(f"Failed to initialize decoder model: {e}")
+
+ # Load checkpoints
+ if cfg.get("geometry_checkpoint_file") is not None:
+ # Direct file path provided
+ geometry_encoder_path = cfg.geometry_checkpoint_file
+ if os.path.exists(geometry_encoder_path):
+ try:
+ checkpoint = torch.load(
+ geometry_encoder_path, map_location="cpu", weights_only=False
+ )
+ geometry_encoder.load_state_dict(checkpoint, strict=True)
+ print(f"โ Loaded geometry encoder from: {geometry_encoder_path}")
+ except Exception as e:
+ raise RuntimeError(
+ f"Failed to load geometry encoder checkpoint from {geometry_encoder_path}: {e}\n"
+ f"This could be due to:\n"
+ f" - Checkpoint corruption\n"
+ f" - Model architecture mismatch\n"
+ f" - Incompatible checkpoint format"
+ )
+ else:
+ raise FileNotFoundError(
+ f"Geometry checkpoint file not found: {geometry_encoder_path}"
+ )
+ elif cfg.load_ckpt is not None:
+ # Directory path provided - use original logic
+ geometry_encoder_path = os.path.join(
+ cfg.load_dir, "ckpts", f"geometry_encoder_step{cfg.load_ckpt:07d}.pt"
+ )
+ if os.path.exists(geometry_encoder_path):
+ try:
+ checkpoint = torch.load(
+ geometry_encoder_path, map_location="cpu", weights_only=False
+ )
+ geometry_encoder.load_state_dict(checkpoint, strict=True)
+ print(f"โ Loaded geometry encoder from: {geometry_encoder_path}")
+ except Exception as e:
+ raise RuntimeError(
+ f"Failed to load geometry encoder checkpoint from {geometry_encoder_path}: {e}\n"
+ f"This could be due to:\n"
+ f" - Checkpoint corruption\n"
+ f" - Model architecture mismatch\n"
+ f" - Incompatible checkpoint format"
+ )
+ else:
+ # This should not happen if _find_ckpt is working correctly, but adding for safety
+ raise FileNotFoundError(
+ f"Geometry encoder checkpoint not found: {geometry_encoder_path}\n"
+ f"Directory contents: {os.listdir(os.path.join(cfg.load_dir, 'ckpts')) if os.path.exists(os.path.join(cfg.load_dir, 'ckpts')) else 'Directory does not exist'}"
+ )
+
+ if decoder is not None:
+ decoder_path = os.path.join(
+ cfg.load_dir, "ckpts", f"decoder_step{cfg.load_ckpt:07d}.pt"
+ )
+ if os.path.exists(decoder_path):
+ try:
+ checkpoint = torch.load(
+ decoder_path, map_location="cpu", weights_only=False
+ )
+ decoder.load_state_dict(checkpoint, strict=True)
+ print(f"โ Loaded decoder from: {decoder_path}")
+ except Exception as e:
+ raise RuntimeError(
+ f"Failed to load decoder checkpoint from {decoder_path}: {e}\n"
+ f"This could be due to:\n"
+ f" - Checkpoint corruption\n"
+ f" - Model architecture mismatch\n"
+ f" - Incompatible checkpoint format"
+ )
+ else:
+ print(f"โ Decoder checkpoint not found (optional): {decoder_path}")
+ else:
+ print(
+ "โ No geometry encoder checkpoint specified - using randomly initialized weights"
+ )
+
+ return geometry_encoder, decoder
+
+ @staticmethod
+ def _load_matvae(cfg: edict, device: torch.device) -> TripletVAE:
+ """
+ Initialize and load pre-trained MatVAE model.
+
+ Creates a TripletVAE instance from configuration and loads pre-trained
+ weights from a safetensors checkpoint file. The MatVAE handles encoding
+ and decoding of material properties (Young's modulus, Poisson's ratio, density).
+
+ Args:
+ cfg: Configuration object containing:
+ - models.matvae.args: MatVAE model architecture arguments
+ - matvae_checkpoint: Path to .safetensors checkpoint file
+ device: Target device for model placement
+
+ Returns:
+ Loaded TripletVAE model ready for inference
+
+ Raises:
+ RuntimeError: If model initialization or checkpoint loading fails
+ FileNotFoundError: If checkpoint file doesn't exist
+
+ Note:
+ - Model is automatically moved to specified device
+ - Uses safetensors format for secure checkpoint loading
+ - Checkpoint loading is strict (all parameters must match)
+ - MatVAE weights are typically frozen during geometry encoder training
+ """
+ try:
+ matvae = TripletVAE(**cfg.models.matvae.args).to(device)
+ except Exception as e:
+ raise RuntimeError(f"Failed to initialize MatVAE model: {e}")
+
+ if cfg.get("matvae_checkpoint") is not None:
+ if not os.path.exists(cfg.matvae_checkpoint):
+ raise FileNotFoundError(
+ f"MatVAE checkpoint file not found: {cfg.matvae_checkpoint}"
+ )
+
+ try:
+ checkpoint = load_file(cfg.matvae_checkpoint)
+ matvae.load_state_dict(checkpoint, strict=True)
+ print(f"โ Loaded MatVAE from: {cfg.matvae_checkpoint}")
+ except Exception as e:
+ raise RuntimeError(
+ f"Failed to load MatVAE checkpoint from {cfg.matvae_checkpoint}: {e}\n"
+ f"This could be due to:\n"
+ f" - Checkpoint corruption\n"
+ f" - Model architecture mismatch\n"
+ f" - Incompatible safetensors format\n"
+ f" - Missing or extra parameters in checkpoint"
+ )
+ else:
+ raise RuntimeError(
+ "No MatVAE checkpoint specified. MatVAE requires pre-trained weights for proper inference."
+ )
+
+ return matvae
+
+ def to(self, device: Union[str, torch.device]) -> "Vomp":
+ """
+ Move all model components to specified device.
+
+ Transfers the geometry encoder, MatVAE, and optional decoder to the
+ target device (CPU/GPU). Useful for switching between devices after
+ model initialization.
+
+ Args:
+ device: Target device specification. Can be:
+ - String: 'cpu', 'cuda', 'cuda:0', etc.
+ - torch.device: Device object
+
+ Returns:
+ Self reference for method chaining
+
+ Example:
+ >>> model = Vomp.from_checkpoint(config_path)
+ >>> model = model.to('cuda:1') # Move to specific GPU
+ """
+ device = torch.device(device)
+
+ self.geometry_encoder = self.geometry_encoder.to(device)
+ self.matvae = self.matvae.to(device)
+ if self.decoder is not None:
+ self.decoder = self.decoder.to(device)
+
+ return self
+
+ @torch.inference_mode()
+ def predict_materials(
+ self,
+ coords: torch.Tensor,
+ features: torch.Tensor,
+ max_voxels: int = 32000,
+ sample_posterior: bool = False,
+ ) -> Dict[str, np.ndarray]:
+ """
+ Predict material properties for given coordinates and features.
+
+ Takes voxel coordinates and their corresponding DINO features and runs them
+ through the geometry encoder and material VAE to predict mechanical properties.
+ Handles memory management by downsampling if too many voxels are provided.
+
+ Args:
+ coords: Voxel coordinates tensor (N, 4) in format [batch_idx, x, y, z]
+ where coordinates are normalized to [-0.5, 0.5] range
+ features: DINO feature vectors (N, feature_dim) extracted from rendered views
+ max_voxels: Maximum number of voxels to process at once. If input exceeds
+ this, random downsampling is applied to manage GPU memory usage
+ sample_posterior: Whether to sample from VAE posterior distribution instead
+ of using mean predictions for more diverse outputs
+
+ Returns:
+ Dictionary containing:
+ - 'youngs_modulus': Young's modulus values in Pascals (N,)
+ - 'poisson_ratio': Poisson's ratio values (N,)
+ - 'density': Density values in kg/mยณ (N,)
+ - 'voxel_coords_world': World space coordinates (N, 3)
+ - 'num_voxels': Total number of processed voxels
+
+ Note:
+ All predictions are automatically denormalized using the loaded
+ normalization parameters to return physically meaningful values.
+ """
+ device = next(self.parameters()).device
+ coords = coords.to(device)
+ features = features.to(device)
+
+ # Downsample if too many voxels
+ num_voxels = coords.shape[0]
+ if num_voxels > max_voxels:
+ print(f"Downsampling from {num_voxels} to {max_voxels} voxels")
+ indices = torch.randperm(num_voxels)[:max_voxels]
+ coords = coords[indices]
+ features = features[indices]
+ num_voxels = max_voxels
+
+ # Create sparse tensor
+ sparse_tensor = SparseTensor(
+ feats=features, coords=coords, shape=(1, 64, 64, 64)
+ )
+
+ print(f"Running inference on {num_voxels} voxels...")
+
+ # Get training mode from config
+ training_mode = self.config.get("training_mode", "encoder_only")
+
+ # Forward pass through geometry encoder
+ z, mean, logvar = self.geometry_encoder(
+ sparse_tensor, sample_posterior=sample_posterior, return_raw=True
+ )
+
+ # Decode materials based on training mode
+ if training_mode == "encoder_only":
+ latent_2d = z.feats
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+ E_pred = E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ nu_pred = nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ rho_pred = rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+
+ elif training_mode == "encoder_decoder_matvae":
+ decoder_output = self.decoder(z)
+ latent_2d = decoder_output.feats
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+ E_pred = E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ nu_pred = nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ rho_pred = rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+
+ elif training_mode == "encoder_decoder_direct":
+ decoder_output = self.decoder(z)
+ material_predictions = decoder_output.feats
+ E_pred = material_predictions[:, 0]
+ nu_pred = material_predictions[:, 1]
+ rho_pred = material_predictions[:, 2]
+
+ else:
+ raise ValueError(f"Unknown training mode: {training_mode}")
+
+ # Denormalize predictions
+ pred_materials_normalized = torch.stack([E_pred, nu_pred, rho_pred], dim=-1)
+ pred_materials_raw = (
+ self.material_transform.destandardize_and_inverse_transform_tensor(
+ pred_materials_normalized
+ )
+ )
+
+ # Convert to numpy
+ E_values = pred_materials_raw[:, 0].cpu().numpy()
+ nu_values = pred_materials_raw[:, 1].cpu().numpy()
+ rho_values = pred_materials_raw[:, 2].cpu().numpy()
+
+ # Calculate world coordinates
+ voxel_indices = coords[:, 1:].cpu().numpy()
+ voxel_coords_world = (voxel_indices + 0.5) / 64 - 0.5
+
+ return {
+ "youngs_modulus": E_values,
+ "poisson_ratio": nu_values,
+ "density": rho_values,
+ "num_voxels": num_voxels,
+ "voxel_coords": coords.cpu().numpy(),
+ "voxel_coords_world": voxel_coords_world,
+ "material_properties_per_voxel": pred_materials_raw.cpu().numpy(),
+ }
+
+ @property
+ def device(self) -> torch.device:
+ """
+ Get the current device of the model.
+
+ Returns the device where the model parameters are currently located.
+ All components (geometry encoder, MatVAE, decoder) should be on the
+ same device.
+
+ Returns:
+ torch.device: Current device of model parameters
+
+ Example:
+ >>> print(f"Model is on: {model.device}")
+ Model is on: cuda:0
+ """
+ return next(self.parameters()).device
+
+ @torch.inference_mode()
+ def run_single_object_inference(
+ self,
+ coords: torch.Tensor,
+ features: torch.Tensor,
+ max_voxels: int = 32000,
+ sample_posterior: bool = False,
+ return_latents: bool = False,
+ ) -> Dict[str, Union[torch.Tensor, np.ndarray]]:
+ """
+ Low-level raw input and low-level raw output inference.
+
+ Args:
+ coords: Voxel coordinates (N, 4) - [batch_idx, x, y, z]
+ features: Feature vectors (N, feature_dim)
+ max_voxels: Maximum number of voxels to process (for memory)
+ sample_posterior: Whether to sample from posterior
+ return_latents: Whether to return raw latent representations
+
+ Returns:
+ Dictionary containing raw model outputs
+ """
+ device = self.device
+ coords = coords.to(device)
+ features = features.to(device)
+
+ # Downsample if too many voxels
+ num_voxels = coords.shape[0]
+ if num_voxels > max_voxels:
+ indices = torch.randperm(num_voxels)[:max_voxels]
+ coords = coords[indices]
+ features = features[indices]
+ num_voxels = max_voxels
+
+ # Create sparse tensor
+ sparse_tensor = SparseTensor(
+ feats=features, coords=coords, shape=(1, 64, 64, 64)
+ )
+
+ # Forward pass through geometry encoder
+ z, mean, logvar = self.geometry_encoder(
+ sparse_tensor, sample_posterior=sample_posterior, return_raw=True
+ )
+
+ result = {
+ "geometry_latent": z.feats,
+ "geometry_mean": mean,
+ "geometry_logvar": logvar,
+ "voxel_coords": coords,
+ "num_voxels": num_voxels,
+ }
+
+ # Get training mode and decode accordingly
+ training_mode = self.config.get("training_mode", "encoder_only")
+
+ if training_mode == "encoder_decoder_matvae" and self.decoder is not None:
+ decoder_output = self.decoder(z)
+ result["decoder_output"] = decoder_output.feats
+ material_latents = decoder_output.feats
+ elif training_mode == "encoder_decoder_direct" and self.decoder is not None:
+ decoder_output = self.decoder(z)
+ result["material_predictions_raw"] = decoder_output.feats
+ return result
+ else:
+ material_latents = z.feats
+
+ # Decode through MatVAE
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = self.matvae.decode(
+ material_latents
+ )
+
+ result.update(
+ {
+ "E_mu": E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu,
+ "E_logvar": E_logvar.squeeze(-1) if E_logvar.dim() > 1 else E_logvar,
+ "nu_mu": nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu,
+ "nu_logvar": (
+ nu_logvar.squeeze(-1) if nu_logvar.dim() > 1 else nu_logvar
+ ),
+ "rho_mu": rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu,
+ "rho_logvar": (
+ rho_logvar.squeeze(-1) if rho_logvar.dim() > 1 else rho_logvar
+ ),
+ }
+ )
+
+ if return_latents:
+ result["material_latents"] = material_latents
+
+ return result
+
+ @torch.inference_mode()
+ def decode_material(
+ self, latent: torch.Tensor, denormalize: bool = True
+ ) -> Dict[str, torch.Tensor]:
+ """
+ Decode material properties from latent representation.
+
+ Args:
+ latent: Latent material representation (N, latent_dim)
+ denormalize: Whether to denormalize to physical units
+
+ Returns:
+ Dictionary with material properties
+ """
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = self.matvae.decode(
+ latent.to(self.device)
+ )
+
+ E_pred = E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ nu_pred = nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ rho_pred = rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+
+ result = {
+ "youngs_modulus": E_pred,
+ "poisson_ratio": nu_pred,
+ "density": rho_pred,
+ "E_logvar": E_logvar.squeeze(-1) if E_logvar.dim() > 1 else E_logvar,
+ "nu_logvar": nu_logvar.squeeze(-1) if nu_logvar.dim() > 1 else nu_logvar,
+ "rho_logvar": (
+ rho_logvar.squeeze(-1) if rho_logvar.dim() > 1 else rho_logvar
+ ),
+ }
+
+ if denormalize:
+ # Denormalize to physical units
+ pred_materials_normalized = torch.stack([E_pred, nu_pred, rho_pred], dim=-1)
+ pred_materials_raw = (
+ self.material_transform.destandardize_and_inverse_transform_tensor(
+ pred_materials_normalized
+ )
+ )
+
+ result.update(
+ {
+ "youngs_modulus_pa": pred_materials_raw[:, 0],
+ "poisson_ratio_raw": pred_materials_raw[:, 1],
+ "density_kg_m3": pred_materials_raw[:, 2],
+ }
+ )
+
+ return result
+
+ @torch.inference_mode()
+ def encode_material(
+ self,
+ youngs_modulus: torch.Tensor,
+ poisson_ratio: torch.Tensor,
+ density: torch.Tensor,
+ normalize: bool = True,
+ ) -> torch.Tensor:
+ """
+ Encode material properties to latent representation.
+
+ Args:
+ youngs_modulus: Young's modulus values (N,) in Pa
+ poisson_ratio: Poisson's ratio values (N,)
+ density: Density values (N,) in kg/mยณ
+ normalize: Whether to normalize from physical units
+
+ Returns:
+ Latent material representation (N, latent_dim)
+ """
+ device = self.device
+ E = youngs_modulus.to(device)
+ nu = poisson_ratio.to(device)
+ rho = density.to(device)
+
+ if normalize:
+ # Stack and normalize
+ materials = torch.stack([E, nu, rho], dim=-1)
+ materials_normalized = (
+ self.material_transform.standardize_and_transform_tensor(materials)
+ )
+ E, nu, rho = (
+ materials_normalized[:, 0],
+ materials_normalized[:, 1],
+ materials_normalized[:, 2],
+ )
+
+ # Encode through MatVAE
+ material_tensor = torch.stack([E, nu, rho], dim=-1)
+ latent_mean, latent_logvar = self.matvae.encode(material_tensor)
+
+ return latent_mean
+
+ @torch.inference_mode()
+ def get_features(
+ self,
+ obj_3d: Any = None,
+ renders_metadata: Optional[List[Dict[str, Any]]] = None,
+ voxel_centers: Optional[np.ndarray] = None,
+ render_func: Optional[RenderFunction] = None,
+ voxelize_func: Optional[VoxelizeFunction] = None,
+ num_views: int = 150,
+ model_name: str = "dinov2_vitl14_reg",
+ batch_size: int = 16,
+ image_size: int = 518,
+ output_dir: Optional[str] = None,
+ save_features: bool = True,
+ **kwargs: Any,
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Extract DINO features from 3D object through rendering and voxelization.
+
+ Core feature extraction supporting custom rendering/voxelization workflows.
+ For Gaussian splats, prefer get_splat_materials() for simplicity.
+
+ Args:
+ obj_3d: 3D object for feature extraction
+ renders_metadata: Pre-computed render metadata list
+ voxel_centers: Pre-computed voxel centers (N, 3)
+ render_func: Custom rendering function (see RenderFunction protocol)
+ voxelize_func: Custom voxelization function (see VoxelizeFunction protocol)
+ num_views: Number of camera views for rendering
+ model_name: DINO model identifier
+ batch_size: Feature extraction batch size
+ image_size: Render target image size
+ output_dir: Directory for intermediate files
+ save_features: Whether to cache features to disk
+ **kwargs: Additional arguments for custom functions
+
+ Returns:
+ Tuple of (voxel_coordinates, dino_features):
+ - voxel_coordinates: (N, 4) tensor [batch_idx, x, y, z]
+ - dino_features: (N, feature_dim) tensor with extracted features
+
+ Raises:
+ ValueError: If required inputs are missing for chosen workflow
+ """
+ if output_dir is None:
+ output_dir = "/tmp/Vomp_features"
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Obtain or generate render metadata
+ if renders_metadata is None:
+ if render_func is not None:
+ # Use custom rendering function
+ if obj_3d is None:
+ raise ValueError(
+ "obj_3d must be provided when using custom render_func"
+ )
+ print("Using custom rendering function...")
+ renders_metadata = render_func(
+ obj_3d, output_dir, num_views, image_size, **kwargs
+ )
+ else:
+ # Try to load existing renders metadata
+ metadata_path = os.path.join(output_dir, "renders_metadata.json")
+ if os.path.exists(metadata_path):
+ with open(metadata_path, "r") as f:
+ renders_metadata = json.load(f)
+ else:
+ raise ValueError(
+ "No renders_metadata provided and no existing metadata found. "
+ "Either provide renders_metadata, a custom render_func, or ensure renders exist."
+ )
+
+ # Obtain or generate voxel centers
+ if voxel_centers is None:
+ if voxelize_func is not None:
+ # Use custom voxelization function
+ if obj_3d is None:
+ raise ValueError(
+ "obj_3d must be provided when using custom voxelize_func"
+ )
+ print("Using custom voxelization function...")
+ voxel_centers = voxelize_func(obj_3d, output_dir, **kwargs)
+ else:
+ # Try to load existing voxels
+ voxel_path = os.path.join(output_dir, "voxels", "voxels.ply")
+ if os.path.exists(voxel_path):
+ voxel_centers = self._load_voxel_centers(voxel_path)
+ else:
+ raise ValueError(
+ "No voxel_centers provided and no existing voxels found. "
+ "Either provide voxel_centers, a custom voxelize_func, or ensure voxels exist."
+ )
+
+ # Step 3: Extract features using DINO
+ coords, features = self._extract_dino_features(
+ output_dir,
+ voxel_centers,
+ renders_metadata,
+ model_name,
+ batch_size,
+ image_size,
+ save_features,
+ )
+
+ return coords, features
+
+ def _sample_camera_views(
+ self,
+ num_views: int,
+ radius: float = 2.0,
+ fov: float = 45.0,
+ seed: Optional[int] = None,
+ ) -> Tuple[List[float], List[float], List[float], List[float]]:
+ """
+ Generate camera viewpoints using quasi-random sampling.
+
+ Uses Hammersley sequence for uniform distribution of camera positions
+ on a sphere around the object. Provides better coverage than random
+ sampling and ensures consistent results across runs.
+
+ Args:
+ num_views: Number of camera viewpoints to generate
+ radius: Distance from object center to camera positions
+ fov: Field of view in degrees for all cameras
+ seed: Random seed for reproducible sampling. If None, uses random offset
+
+ Returns:
+ Tuple containing:
+ - yaws: List of yaw angles in radians
+ - pitchs: List of pitch angles in radians
+ - radius_list: List of camera distances (all same value)
+ - fov_list: List of FOV values in degrees (all same value)
+ """
+ if seed is not None:
+ np.random.seed(seed)
+
+ yaws = []
+ pitchs = []
+ offset = (np.random.rand(), np.random.rand())
+
+ # Prime numbers for Halton sequence generation
+ PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]
+
+ def radical_inverse(base, n):
+ val = 0
+ inv_base = 1.0 / base
+ inv_base_n = inv_base
+ while n > 0:
+ digit = n % base
+ val += digit * inv_base_n
+ n //= base
+ inv_base_n *= inv_base
+ return val
+
+ def halton_sequence(dim, n):
+ return [radical_inverse(PRIMES[d], n) for d in range(dim)]
+
+ def hammersley_sequence(dim, n, num_samples):
+ return [n / num_samples] + halton_sequence(dim - 1, n)
+
+ def sphere_hammersley_sequence(n, num_samples, offset=(0, 0)):
+ u, v = hammersley_sequence(2, n, num_samples)
+ u += offset[0] / num_samples
+ v += offset[1]
+ u = 2 * u if u < 0.25 else 2 / 3 * u + 1 / 3
+ theta = np.arccos(1 - 2 * u) - np.pi / 2
+ phi = v * 2 * np.pi
+ return [phi, theta]
+
+ for i in range(num_views):
+ y, p = sphere_hammersley_sequence(i, num_views, offset)
+ yaws.append(y)
+ pitchs.append(p)
+
+ radius_list = [radius] * num_views
+ fov_list = [fov] * num_views
+
+ return yaws, pitchs, radius_list, fov_list
+
+ @torch.inference_mode()
+ def render_sampled_views(
+ self,
+ gaussian_model: "Gaussian",
+ output_dir: str,
+ num_views: int = 150,
+ image_size: int = 518,
+ radius: float = 2.0,
+ fov: float = 40.0,
+ seed: Optional[int] = None,
+ ) -> List[Dict]:
+ """
+ Render multiple views of Gaussian splat from sampled camera positions.
+
+ Generates camera viewpoints using quasi-random sampling and renders
+ the Gaussian splat model from each position. Saves RGBA images and
+ camera metadata in NeRF-compatible format.
+
+ Args:
+ gaussian_model: Gaussian splat model to render
+ output_dir: Directory to save rendered images and metadata
+ num_views: Number of camera views to render
+ image_size: Resolution of rendered images (square)
+ radius: Camera distance from object center
+ fov: Field of view in degrees
+ seed: Random seed for reproducible camera sampling
+
+ Returns:
+ List of frame metadata dictionaries containing:
+ - file_path: Relative path to rendered image
+ - transform_matrix: 4x4 camera-to-world matrix
+ - camera_angle_x: FOV in radians
+ - yaw, pitch, radius: Camera positioning parameters
+ """
+ renders_dir = os.path.join(output_dir, "renders")
+ os.makedirs(renders_dir, exist_ok=True)
+
+ print(f"Rendering {num_views} Gaussian splat views...")
+
+ # Sample camera views using shared utility
+ yaws, pitchs, radius_list, fov_list = self._sample_camera_views(
+ num_views, radius, fov, seed
+ )
+ print("sampled camera views")
+
+ # Generate camera extrinsics and intrinsics matrices
+ extrinsics_list, intrinsics_list = yaw_pitch_r_fov_to_extrinsics_intrinsics(
+ yaws, pitchs, radius_list, fov_list
+ )
+ print("generated camera extrinsics and intrinsics matrices")
+
+ frames_metadata = []
+ for i, (extrinsics, intrinsics) in enumerate(
+ zip(extrinsics_list, intrinsics_list)
+ ):
+ # Create camera configuration for rendering
+ fovx = 2 * torch.atan(0.5 / intrinsics[0, 0])
+ fovy = 2 * torch.atan(0.5 / intrinsics[1, 1])
+ camera_center = torch.inverse(extrinsics)[:3, 3]
+
+ camera = edict(
+ {
+ "image_height": image_size,
+ "image_width": image_size,
+ "FoVx": fovx,
+ "FoVy": fovy,
+ "znear": 0.1,
+ "zfar": 100.0,
+ "world_view_transform": extrinsics.T.contiguous(),
+ "projection_matrix": torch.eye(4, device=self.device),
+ "full_proj_transform": extrinsics.T.contiguous(),
+ "camera_center": camera_center,
+ }
+ )
+
+ # Render the Gaussian splat
+ bg_color = torch.tensor(
+ [1.0, 1.0, 1.0], dtype=torch.float32, device=self.device
+ )
+ with torch.inference_mode():
+ render_result = self._simple_render(
+ gaussian_model, camera, bg_color, render_alpha=True
+ )
+ rendered_rgb = render_result["render"]
+ rendered_alpha = render_result.get(
+ "alpha", torch.ones_like(rendered_rgb[:1])
+ )
+
+ # Combine RGB and alpha channels
+ rendered_rgba = torch.cat([rendered_rgb, rendered_alpha], dim=0)
+ rendered_rgba = rendered_rgba.cpu().numpy()
+ rendered_rgba = np.transpose(rendered_rgba, (1, 2, 0))
+ rendered_rgba = np.clip(rendered_rgba * 255, 0, 255).astype(np.uint8)
+
+ # Save rendered image
+ filename = f"{i:04d}.png"
+ image_path = os.path.join(renders_dir, filename)
+ Image.fromarray(rendered_rgba, "RGBA").save(image_path)
+
+ # Create camera-to-world transform matrix for metadata
+ c2w = torch.inverse(extrinsics).cpu().numpy()
+ c2w[:3, 1:3] *= -1
+
+ frame_metadata = {
+ "file_path": filename,
+ "transform_matrix": c2w.tolist(),
+ "camera_angle_x": np.radians(fov),
+ "yaw": yaws[i],
+ "pitch": pitchs[i],
+ "radius": radius,
+ }
+ frames_metadata.append(frame_metadata)
+
+ # DEBUG (temporary): confirm 150-view splat render loop completed; inspect GPU state before DINO/voxelize
+ _n_g = int(gaussian_model.get_xyz.shape[0])
+ _dbg = (
+ f"[VoMP DEBUG] render_sampled_views: finished {num_views}-view loop | "
+ f"n_gaussians={_n_g} | image_size={image_size} | renders_dir={renders_dir!r}"
+ )
+ if torch.cuda.is_available():
+ torch.cuda.synchronize()
+ _dbg += (
+ f" | cuda_alloc_mb={torch.cuda.memory_allocated() / 1e6:.2f}"
+ f" | cuda_reserved_mb={torch.cuda.memory_reserved() / 1e6:.2f}"
+ f" | cuda_peak_alloc_mb={torch.cuda.max_memory_allocated() / 1e6:.2f}"
+ )
+ print(_dbg, flush=True)
+
+ # Save NeRF-compatible transforms.json
+ transforms_data = {
+ "camera_angle_x": np.radians(fov),
+ "frames": frames_metadata,
+ }
+
+ transforms_path = os.path.join(renders_dir, "transforms.json")
+ with open(transforms_path, "w") as f:
+ json.dump(transforms_data, f, indent=2)
+
+ # Save metadata for pipeline compatibility
+ metadata_path = os.path.join(output_dir, "renders_metadata.json")
+ with open(metadata_path, "w") as f:
+ json.dump(frames_metadata, f, indent=2)
+
+ print(f"โ Rendered {num_views} views")
+ return frames_metadata
+
+ @torch.inference_mode()
+ def get_splat_materials(
+ self,
+ gaussian_model: Union["Gaussian", str],
+ output_dir: Optional[str] = None,
+ num_views: int = 150,
+ image_size: int = 518,
+ render_image_size: int = 512,
+ radius: float = 2.0,
+ fov: float = 40.0,
+ seed: Optional[int] = None,
+ sh_degree: int = 3,
+ aabb: Optional[List[float]] = None,
+ device: Optional[Union[str, torch.device]] = None,
+ voxel_method: str = "centers",
+ voxel_level: int = 6,
+ voxel_iso: float = 11.345,
+ voxel_tol: float = 1.0 / 8.0,
+ voxel_step: int = 10,
+ voxel_opacity_threshold: float = 0.35,
+ max_voxels: Optional[int] = 32768,
+ query_points: Union[str, np.ndarray, None] = "splat_centers",
+ dino_batch_size: int = 16,
+ **kwargs: Any,
+ ) -> Dict[str, np.ndarray]:
+ """
+ High-level API for Gaussian splat material property inference.
+
+ Automatically handles PLY loading, rendering, voxelization, material prediction,
+ and upsampling for Gaussian splat representations.
+
+ Args:
+ gaussian_model: Gaussian object or PLY file path
+ output_dir: Directory for intermediate files
+ num_views: Camera views for rendering (default: 150, matches training data)
+ image_size: Target image size for DINO feature extraction. Rendered images are
+ resized to this resolution before processing (default: 518)
+ render_image_size: Resolution for rendering. Set to 512 to match training
+ data pipeline where images were rendered at 512x512 then resized to 518x518
+ for DINO feature extraction (default: 512)
+ radius: Camera distance from object center (default: 2.0, matches training data)
+ fov: Field of view in degrees (default: 40.0, matches training data)
+ seed: Random seed for camera view sampling
+ sh_degree: Spherical harmonics degree for PLY loading
+ aabb: Bounding box for PLY loading (defaults to [-1,-1,-1,2,2,2])
+ device: Compute device (defaults to CUDA if available)
+ voxel_method: Voxelization method ('centers' or 'kaolin')
+ - 'centers': Simple center-based voxelization (fast, less accurate)
+ - 'kaolin': Full Gaussian voxelization using kaolin (slower, more accurate)
+ voxel_level: Voxel grid resolution level for kaolin voxelization (2^level), default 6 for 64^3
+ voxel_iso: Iso value for kaolin voxelization
+ voxel_tol: Tolerance for kaolin voxelization
+ voxel_step: Number of samples for opacity integration in kaolin method
+ voxel_opacity_threshold: Minimum opacity threshold for voxels in kaolin method
+ max_voxels: Maximum number of voxels to process (for memory management)
+ query_points: Where to evaluate material properties. Can be:
+ - "splat_centers" (default): Evaluate at Gaussian splat centers
+ - "voxel_centers": Evaluate at voxel centers (original behavior)
+ - numpy array (N, 3): Custom 3D coordinates for evaluation
+ - None: Same as "voxel_centers"
+ dino_batch_size: Number of images to process simultaneously during DINO feature extraction (higher values use more GPU memory but may be faster)
+ **kwargs: Additional feature extraction arguments
+
+ Returns:
+ Dictionary containing material properties:
+ When query_points="splat_centers" or custom array:
+ - 'youngs_modulus': Young's modulus values at query points (Pa)
+ - 'poisson_ratio': Poisson's ratio values at query points
+ - 'density': Density values at query points (kg/mยณ)
+ - 'query_coords_world': World coordinates of query points
+ - 'voxel_coords_world': World coordinates of voxels (for reference)
+ - 'query_distances': Distance from each query point to nearest voxel
+ - 'num_voxels', 'num_query_points': Counts for reference
+
+ When query_points="voxel_centers" or None:
+ - 'youngs_modulus': Young's modulus values at voxel centers (Pa)
+ - 'poisson_ratio': Poisson's ratio values at voxel centers
+ - 'density': Density values at voxel centers (kg/mยณ)
+ - 'voxel_coords_world': World coordinates of voxels
+ - 'num_voxels': Number of voxels
+ """
+ # Handle automatic Gaussian loading from PLY path
+ if isinstance(gaussian_model, str):
+ # Set defaults
+ if aabb is None:
+ aabb = [-1, -1, -1, 2, 2, 2]
+ if device is None:
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+
+ # Create and load Gaussian
+ ply_path = gaussian_model
+ gaussian_model = Gaussian(sh_degree=sh_degree, aabb=aabb, device=device)
+ gaussian_model.load_ply(ply_path)
+
+ if output_dir is None:
+ output_dir = f"/tmp/Vomp_splat_{id(gaussian_model)}"
+
+ print("=== Vomp: Splat Material Estimation ===")
+
+ # Define built-in splat rendering function
+ def splat_render_func(
+ gaussian_model, output_dir, num_views, image_size, **kwargs
+ ):
+ # Filter kwargs to only pass render-related parameters
+ render_kwargs = {
+ k: v for k, v in kwargs.items() if k in ["radius", "fov", "seed"]
+ }
+ # Use render_image_size for actual rendering (default 512 to match training data)
+ return self.render_sampled_views(
+ gaussian_model,
+ output_dir,
+ num_views,
+ render_image_size,
+ **render_kwargs,
+ )
+
+ # Define built-in splat voxelization function
+ def splat_voxelize_func(gaussian_model, output_dir, **kwargs):
+ # Extract voxelization parameters from kwargs
+ voxel_method = kwargs.get("voxel_method", "centers")
+ voxel_level = kwargs.get("voxel_level", 6)
+ voxel_iso = kwargs.get("voxel_iso", 11.345)
+ voxel_tol = kwargs.get("voxel_tol", 1.0 / 8.0)
+ voxel_step = kwargs.get("voxel_step", 10)
+ voxel_opacity_threshold = kwargs.get("voxel_opacity_threshold", 0.35)
+
+ return self._voxelize_gaussian(
+ gaussian_model,
+ output_dir,
+ method=voxel_method,
+ level=voxel_level,
+ iso=voxel_iso,
+ tol=voxel_tol,
+ step=voxel_step,
+ opacity_threshold=voxel_opacity_threshold,
+ )
+
+ # Step 1: Extract features using built-in splat functions
+ print("Step 1: Extracting features...")
+
+ # Normalize Gaussian to standard coordinate system
+ gaussian_model = self._normalize_gaussian(gaussian_model)
+
+ # Prepare voxelization parameters for kwargs
+ voxel_kwargs = {
+ "voxel_method": voxel_method,
+ "voxel_level": voxel_level,
+ "voxel_iso": voxel_iso,
+ "voxel_tol": voxel_tol,
+ "voxel_step": voxel_step,
+ "voxel_opacity_threshold": voxel_opacity_threshold,
+ }
+
+ # Prepare rendering parameters for kwargs
+ render_kwargs = {
+ "radius": radius,
+ "fov": fov,
+ "seed": seed,
+ }
+
+ coords, features = self.get_features(
+ obj_3d=gaussian_model,
+ render_func=splat_render_func,
+ voxelize_func=splat_voxelize_func,
+ num_views=num_views,
+ image_size=image_size,
+ output_dir=output_dir,
+ batch_size=dino_batch_size,
+ **voxel_kwargs,
+ **render_kwargs,
+ **kwargs,
+ )
+
+ # Step 2: Run inference on the features
+ print("Step 2: Running material inference...")
+ predict_kwargs = {}
+ if max_voxels is not None:
+ predict_kwargs["max_voxels"] = max_voxels
+ voxel_results = self.predict_materials(coords, features, **predict_kwargs)
+
+ # Add splat count for reporting
+ num_splats = int(gaussian_model.get_xyz.shape[0])
+ voxel_results["num_splats"] = num_splats
+
+ # Handle query_points parameter
+ if query_points == "splat_centers":
+ # Step 3: Evaluate materials at splat centers
+ print("Step 3: Evaluating materials at splat centers...")
+
+ # Create upsampler from voxel results
+ upsampler = MaterialUpsampler(
+ voxel_coords=voxel_results["voxel_coords_world"],
+ voxel_materials=np.column_stack(
+ [
+ voxel_results["youngs_modulus"],
+ voxel_results["poisson_ratio"],
+ voxel_results["density"],
+ ]
+ ),
+ )
+
+ # Interpolate to splat centers
+ query_materials, query_distances = upsampler.interpolate_to_gaussians(
+ gaussian_model
+ )
+
+ # Create final results with splat-level materials
+ results = {
+ "youngs_modulus": query_materials[:, 0],
+ "poisson_ratio": query_materials[:, 1],
+ "density": query_materials[:, 2],
+ "query_coords_world": gaussian_model.get_xyz.detach().cpu().numpy(),
+ "query_distances": query_distances,
+ "voxel_coords_world": voxel_results[
+ "voxel_coords_world"
+ ], # Keep for reference
+ "num_voxels": voxel_results["num_voxels"],
+ "num_query_points": num_splats,
+ }
+
+ print(f"โ Evaluated materials at {num_splats:,} splat centers")
+ elif query_points == "voxel_centers" or query_points is None:
+ # Return voxel-level results (original behavior)
+ results = voxel_results
+ elif isinstance(query_points, np.ndarray):
+ # Step 3: Evaluate materials at custom query points
+ print(
+ f"Step 3: Evaluating materials at {len(query_points)} custom query points..."
+ )
+
+ # Create upsampler from voxel results
+ upsampler = MaterialUpsampler(
+ voxel_coords=voxel_results["voxel_coords_world"],
+ voxel_materials=np.column_stack(
+ [
+ voxel_results["youngs_modulus"],
+ voxel_results["poisson_ratio"],
+ voxel_results["density"],
+ ]
+ ),
+ )
+
+ # Interpolate to custom query points
+ query_materials, query_distances = upsampler.interpolate(query_points)
+
+ # Create final results with custom query point materials
+ results = {
+ "youngs_modulus": query_materials[:, 0],
+ "poisson_ratio": query_materials[:, 1],
+ "density": query_materials[:, 2],
+ "query_coords_world": query_points, # Primary coordinates for this case
+ "voxel_coords_world": voxel_results[
+ "voxel_coords_world"
+ ], # Keep for reference
+ "query_distances": query_distances,
+ "num_voxels": voxel_results["num_voxels"],
+ "num_query_points": len(query_points),
+ }
+
+ print(f"โ Evaluated materials at {len(query_points):,} custom query points")
+ else:
+ raise ValueError(
+ f"Invalid query_points value: {query_points}. Must be 'splat_centers', 'voxel_centers', None, or numpy array."
+ )
+
+ print("โ Material estimation complete!")
+ return results
+
+ @torch.inference_mode()
+ def get_custom_materials(
+ self,
+ obj_3d: Any,
+ render_func: callable,
+ voxelize_func: callable,
+ output_dir: Optional[str] = None,
+ **kwargs,
+ ) -> Dict[str, np.ndarray]:
+ """
+ High-level API for custom 3D representations with custom render/voxelize functions.
+
+ This method demonstrates how to use your own 3D representation with custom
+ rendering and voxelization functions.
+
+ Args:
+ obj_3d: Your custom 3D object/representation
+ render_func: Custom rendering function with signature:
+ render_func(obj_3d, output_dir, num_views, image_size, **kwargs) -> List[Dict]
+ voxelize_func: Custom voxelization function with signature:
+ voxelize_func(obj_3d, output_dir, **kwargs) -> np.ndarray
+ output_dir: Output directory for intermediate files
+ **kwargs: Additional arguments passed to render_func and voxelize_func
+
+ Returns:
+ Dictionary with material properties
+
+ Example usage:
+ def my_render_func(my_mesh, output_dir, num_views, image_size, **kwargs):
+ # Your rendering logic here
+ return frames_metadata # List[Dict] with required keys
+
+ def my_voxelize_func(my_mesh, output_dir, **kwargs):
+ # Your voxelization logic here
+ return voxel_centers # np.ndarray shape (N, 3)
+
+ results = model.get_custom_materials(
+ obj_3d=my_mesh,
+ render_func=my_render_func,
+ voxelize_func=my_voxelize_func
+ )
+ """
+ if output_dir is None:
+ output_dir = f"/tmp/Vomp_custom_{id(obj_3d)}"
+
+ print("=== Vomp: Custom Material Estimation ===")
+
+ # Step 1: Extract features using custom functions
+ print("Step 1: Extracting features with custom functions...")
+ coords, features = self.get_features(
+ obj_3d=obj_3d,
+ render_func=render_func,
+ voxelize_func=voxelize_func,
+ output_dir=output_dir,
+ **kwargs,
+ )
+
+ # Step 2: Run inference on the features
+ print("Step 2: Running material inference...")
+ predict_kwargs = {}
+ if "max_voxels" in kwargs and kwargs["max_voxels"] is not None:
+ predict_kwargs["max_voxels"] = kwargs["max_voxels"]
+ results = self.predict_materials(coords, features, **predict_kwargs)
+
+ print("โ Material estimation complete!")
+ return results
+
+ def load_features(self, features_path: str) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Load previously saved DINO features from disk.
+
+ Args:
+ features_path: Path to saved features NPZ file
+
+ Returns:
+ Tuple of (coordinates, features) tensors
+ """
+ if not os.path.exists(features_path):
+ raise FileNotFoundError(f"Features file not found: {features_path}")
+
+ return self._load_saved_features(features_path)
+
+ # Helper methods
+ @torch.inference_mode()
+ def _simple_render(
+ self, gaussian_model: "Gaussian", camera, bg_color, render_alpha=True
+ ) -> Dict[str, torch.Tensor]:
+ """
+ Render single view of Gaussian splat using differentiable rasterization.
+
+ Core rendering function that rasterizes Gaussian splats to images using
+ GPU-accelerated splatting. Supports both RGB and alpha channel rendering
+ for proper transparency handling.
+
+ Args:
+ gaussian_model: Gaussian splat model containing positions, colors, etc.
+ camera: Camera configuration object with intrinsics and extrinsics
+ bg_color: Background color as 3D tensor [R, G, B]
+ render_alpha: Whether to compute alpha channel for transparency
+
+ Returns:
+ Dictionary containing render outputs:
+ - 'render': RGB image tensor (3, H, W)
+ - 'alpha': Alpha channel tensor (1, H, W) if render_alpha=True
+ - 'viewspace_points': Screen space points for gradients
+ - 'visibility_filter': Mask of visible Gaussians
+ - 'radii': Projected radii of Gaussians
+ """
+
+ # Initialize screen space points for gradients
+ screenspace_points = (
+ torch.zeros_like(
+ gaussian_model.get_xyz,
+ dtype=gaussian_model.get_xyz.dtype,
+ requires_grad=True,
+ device=self.device,
+ )
+ + 0
+ )
+ try:
+ screenspace_points.retain_grad()
+ except:
+ pass
+
+ # Calculate FOV
+ tanfovx = math.tan(camera.FoVx * 0.5)
+ tanfovy = math.tan(camera.FoVy * 0.5)
+
+ # Rasterization settings
+ raster_settings = GaussianRasterizationSettings(
+ image_height=int(camera.image_height),
+ image_width=int(camera.image_width),
+ tanfovx=tanfovx,
+ tanfovy=tanfovy,
+ bg=bg_color,
+ scale_modifier=1.0,
+ viewmatrix=camera.world_view_transform,
+ projmatrix=camera.full_proj_transform,
+ sh_degree=gaussian_model.active_sh_degree,
+ campos=camera.camera_center,
+ prefiltered=False,
+ debug=False,
+ )
+
+ rasterizer = GaussianRasterizer(raster_settings=raster_settings)
+
+ means3D = gaussian_model.get_xyz
+ means2D = screenspace_points
+ opacity = gaussian_model.get_opacity
+ scales = gaussian_model.get_scaling
+ rotations = gaussian_model.get_rotation
+ shs = gaussian_model.get_features
+
+ # Render with background color
+ rendered_rgb, radii = rasterizer(
+ means3D=means3D,
+ means2D=means2D,
+ shs=shs,
+ colors_precomp=None,
+ opacities=opacity,
+ scales=scales,
+ rotations=rotations,
+ cov3D_precomp=None,
+ )
+
+ result = {
+ "render": rendered_rgb,
+ "viewspace_points": screenspace_points,
+ "visibility_filter": radii > 0,
+ "radii": radii,
+ }
+
+ if render_alpha:
+ # Render with black background to compute alpha channel
+ black_bg = torch.zeros(3, dtype=torch.float32, device=self.device)
+ raster_settings_black = GaussianRasterizationSettings(
+ image_height=int(camera.image_height),
+ image_width=int(camera.image_width),
+ tanfovx=tanfovx,
+ tanfovy=tanfovy,
+ bg=black_bg,
+ scale_modifier=1.0,
+ viewmatrix=camera.world_view_transform,
+ projmatrix=camera.full_proj_transform,
+ sh_degree=gaussian_model.active_sh_degree,
+ campos=camera.camera_center,
+ prefiltered=False,
+ debug=False,
+ )
+
+ rasterizer_black = GaussianRasterizer(raster_settings=raster_settings_black)
+ rendered_black, _ = rasterizer_black(
+ means3D=means3D,
+ means2D=means2D,
+ shs=shs,
+ colors_precomp=None,
+ opacities=opacity,
+ scales=scales,
+ rotations=rotations,
+ cov3D_precomp=None,
+ )
+
+ # Compute alpha from difference between white and black backgrounds
+ alpha = 1.0 - (rendered_rgb - rendered_black).mean(dim=0, keepdim=True)
+ alpha = torch.clamp(alpha, 0.0, 1.0)
+
+ result["alpha"] = alpha
+
+ return result
+
+ def _normalize_gaussian(self, gaussian_model: "Gaussian") -> "Gaussian":
+ """
+ Normalize Gaussian splat to standard coordinate system.
+
+ Transforms the Gaussian splat coordinates and scaling parameters to fit
+ within the [-0.5, 0.5] coordinate system used by the model. This ensures
+ consistent spatial encoding regardless of original object scale.
+
+ Args:
+ gaussian_model: Input Gaussian splat model to normalize
+
+ Returns:
+ Modified Gaussian model with normalized coordinates and scaling
+ """
+ xyz = gaussian_model.get_xyz
+ min_vals = xyz.min(dim=0, keepdim=True)[0]
+ max_vals = xyz.max(dim=0, keepdim=True)[0]
+ center = (min_vals + max_vals) / 2
+ extent = (max_vals - min_vals).max()
+ scale_factor = 0.98 / extent
+
+ normalized_xyz = (xyz - center) * scale_factor
+ normalized_scaling = gaussian_model.get_scaling * scale_factor
+
+ gaussian_model.from_xyz(normalized_xyz)
+ gaussian_model.from_scaling(normalized_scaling)
+
+ return gaussian_model
+
+ def _voxelize_gaussian(
+ self,
+ gaussian_model: "Gaussian",
+ output_dir: str,
+ method: str = "centers",
+ level: int = 6,
+ iso: float = 11.345,
+ tol: float = 1.0 / 8.0,
+ step: int = 10,
+ opacity_threshold: float = 0.35,
+ ) -> np.ndarray:
+ """
+ Convert Gaussian splat to discrete voxel representation.
+
+ Transforms continuous Gaussian splat representation into discrete voxels
+ for material property prediction. Supports two methods with different
+ accuracy/speed trade-offs.
+
+ Args:
+ gaussian_model: Gaussian splat model with normalized coordinates
+ output_dir: Directory to save voxel data and metadata
+ method: Voxelization algorithm:
+ - 'centers': Fast, uses only Gaussian center positions
+ - 'kaolin': Accurate, integrates full Gaussian parameters
+ level: Voxel grid resolution as power of 2 (2^level grid size)
+ iso: Iso-surface value for kaolin voxelization
+ tol: Numerical tolerance for kaolin voxelization
+ step: Number of integration samples per voxel (kaolin only)
+ opacity_threshold: Minimum opacity to include voxel (kaolin only)
+
+ Returns:
+ Array of voxel center coordinates in world space (N, 3)
+ """
+ voxels_dir = os.path.join(output_dir, "voxels")
+ os.makedirs(voxels_dir, exist_ok=True)
+
+ if method == "centers":
+ return self._voxelize_gaussian_centers(gaussian_model, voxels_dir)
+ elif method == "kaolin":
+ return self._voxelize_gaussian_kaolin(
+ gaussian_model, voxels_dir, level, iso, tol, step, opacity_threshold
+ )
+ else:
+ raise ValueError(
+ f"Unknown voxelization method: {method}. Use 'centers' or 'kaolin'"
+ )
+
+ def _voxelize_gaussian_centers(
+ self, gaussian_model: "Gaussian", voxels_dir: str
+ ) -> np.ndarray:
+ """
+ Fast voxelization using only Gaussian center positions.
+
+ Converts Gaussian splat centers to discrete voxel grid by quantizing
+ positions to nearest voxel cells. Simple and fast but doesn't consider
+ Gaussian shape or opacity information.
+
+ Args:
+ gaussian_model: Gaussian splat model with normalized coordinates
+ voxels_dir: Directory to save voxel PLY file
+
+ Returns:
+ Array of unique voxel center coordinates (N, 3)
+ """
+ # Get Gaussian centers in CPU numpy
+ centers = gaussian_model.get_xyz.detach().cpu().numpy()
+
+ # Handle empty case
+ if len(centers) == 0:
+ print("No Gaussians to voxelize")
+ return np.empty((0, 3), dtype=np.float32)
+
+ # Clamp coordinates to valid range
+ if centers.min() < -0.5 or centers.max() > 0.5:
+ centers = np.clip(centers, -0.5 + 1e-6, 0.5 - 1e-6)
+
+ # Convert world coordinates to 64x64x64 voxel indices
+ voxel_indices = ((centers + 0.5) * 64).astype(np.int32)
+ voxel_indices = np.clip(voxel_indices, 0, 63)
+
+ # Unique voxel indices, then convert back to world-space voxel centers
+ unique_indices = np.unique(voxel_indices, axis=0)
+ voxel_centers = unique_indices.astype(np.float32) / 64.0 - 0.5
+
+ # Save as PLY for compatibility
+ voxel_path = os.path.join(voxels_dir, "voxels.ply")
+ self._save_voxels_ply(voxel_centers, voxel_path)
+
+ print(f"Voxelized to {len(voxel_centers)} voxels (centers method)")
+ return voxel_centers
+
+ def _voxelize_gaussian_kaolin(
+ self,
+ gaussian_model: "Gaussian",
+ voxels_dir: str,
+ level: int = 6,
+ iso: float = 11.345,
+ tol: float = 1.0 / 8.0,
+ step: int = 10,
+ opacity_threshold: float = 0.35,
+ ) -> np.ndarray:
+ """
+ Accurate voxelization using full Gaussian parameters with Kaolin.
+
+ Uses Kaolin's GPU-accelerated Gaussian-to-voxel conversion that considers
+ complete Gaussian parameters (position, scale, rotation, opacity) to
+ compute accurate voxel occupancy through numerical integration.
+
+ Args:
+ gaussian_model: Gaussian splat model with normalized coordinates
+ voxels_dir: Directory to save voxel data and opacity information
+ level: Voxel grid resolution level (2^level), e.g., 6 โ 64ยณ grid
+ iso: Iso-surface value for voxel generation
+ tol: Numerical tolerance for integration accuracy
+ step: Number of sample points per voxel for opacity integration
+ opacity_threshold: Minimum opacity required to include voxel
+
+ Returns:
+ Array of voxel center coordinates for high-opacity regions (N, 3)
+
+ Note: Slower but recommended for accurate material prediction
+ """
+ # Extract Gaussian parameters
+ xyz = gaussian_model.get_xyz # (N, 3)
+ scales = gaussian_model.get_scaling # (N, 3)
+ rots = gaussian_model.get_rotation # (N, 4) quaternions
+ opacities = gaussian_model.get_opacity # (N,)
+
+ # Handle empty case
+ if xyz.shape[0] == 0:
+ print("No Gaussians to voxelize")
+ return np.empty((0, 3), dtype=np.float32)
+
+ print(f"Using Kaolin voxelization with {xyz.shape[0]} Gaussians...")
+ print(f"Resolution: {2**level}^3, opacity_threshold: {opacity_threshold}")
+
+ # Use Kaolin's Gaussian to voxel grid conversion
+ voxel_coords, voxel_opacities = gs_ops.gs_to_voxelgrid(
+ xyz, scales, rots, opacities, level=level, iso=iso, tol=tol, step=step
+ )
+
+ # Filter by opacity threshold
+ mask = voxel_opacities >= opacity_threshold
+ voxel_coords = voxel_coords[mask].contiguous()
+ voxel_opacities = voxel_opacities[mask].contiguous()
+
+ # Handle case where no voxels meet the threshold
+ if voxel_coords.shape[0] == 0:
+ print(
+ f"Voxelized to 0 voxels (kaolin method) - no voxels above threshold {opacity_threshold}"
+ )
+ return np.empty((0, 3), dtype=np.float32)
+
+ # Convert voxel coordinates to world space centers
+ # Kaolin returns integer voxel coordinates, convert to world space [-0.5, 0.5]
+ voxel_coords_cpu = voxel_coords.cpu().numpy()
+ voxel_centers = voxel_coords_cpu.astype(np.float32) / (2**level) - 0.5
+
+ # Save as PLY for compatibility
+ voxel_path = os.path.join(voxels_dir, "voxels.ply")
+ self._save_voxels_ply(voxel_centers, voxel_path)
+
+ # Also save opacity information for analysis
+ voxel_opacity_path = os.path.join(voxels_dir, "voxel_opacities.npz")
+ np.savez_compressed(
+ voxel_opacity_path,
+ voxel_coords=voxel_coords_cpu,
+ voxel_opacities=voxel_opacities.cpu().numpy(),
+ voxel_centers=voxel_centers,
+ )
+
+ print(f"Voxelized to {len(voxel_centers)} voxels (kaolin method)")
+ print(
+ f"Opacity range: [{voxel_opacities.min():.3f}, {voxel_opacities.max():.3f}]"
+ )
+ # DEBUG (temporary): kaolin path succeeded; next step is DINO + predict_materials
+ _dbg2 = (
+ f"[VoMP DEBUG] _voxelize_gaussian_kaolin: done | n_voxels={len(voxel_centers)} | "
+ f"level={level} (grid {2**level}^3)"
+ )
+ if torch.cuda.is_available():
+ torch.cuda.synchronize()
+ _dbg2 += f" | cuda_alloc_mb={torch.cuda.memory_allocated() / 1e6:.2f}"
+ print(_dbg2, flush=True)
+ return voxel_centers
+
+ def _save_voxels_ply(self, voxel_centers: np.ndarray, output_path: str):
+ """
+ Save voxel centers to PLY format file.
+
+ Exports voxel center coordinates to PLY format for visualization
+ and pipeline compatibility. Creates directory structure as needed.
+
+ Args:
+ voxel_centers: Array of 3D voxel center positions (N, 3)
+ output_path: Full path for output PLY file
+ """
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+ write_ply_vertices(voxel_centers, output_path, binary=True)
+
+ def _load_voxel_centers(self, voxel_path: str) -> np.ndarray:
+ """
+ Load voxel center coordinates from PLY format file.
+
+ Reads previously saved voxel positions for feature extraction
+ or analysis. Compatible with files created by _save_voxels_ply.
+
+ Args:
+ voxel_path: Path to PLY file containing voxel data
+
+ Returns:
+ Array of 3D voxel center positions (N, 3)
+
+ Raises:
+ FileNotFoundError: If PLY file doesn't exist
+ ValueError: If PLY file format is invalid
+ """
+ return read_ply_vertices(voxel_path)
+
+ def _extract_dino_features(
+ self,
+ output_dir: str,
+ voxel_centers: np.ndarray,
+ frames_metadata: List[Dict],
+ model_name: str = "dinov2_vitl14_reg",
+ batch_size: int = 16,
+ image_size: int = 518,
+ save_features: bool = True,
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Extract visual features from rendered images using DINO vision transformer.
+
+ Projects voxel positions to image coordinates across multiple views and
+ samples DINO patch features at those locations. Features are aggregated
+ across views to create robust visual representations for each voxel.
+
+ Args:
+ output_dir: Directory containing rendered images and to save features
+ voxel_centers: 3D positions of voxels in world coordinates (N, 3)
+ frames_metadata: List of frame info with camera transforms and paths
+ model_name: DINO model variant to use for feature extraction
+ batch_size: Number of images to process simultaneously
+ image_size: Expected size of input images (assumed square)
+ save_features: Whether to cache extracted features to disk
+
+ Returns:
+ Tuple of (coordinates, features):
+ - coordinates: Voxel indices in sparse tensor format (N, 4)
+ - features: Aggregated DINO features per voxel (N, feature_dim)
+ """
+
+ # Create features directory
+ features_dir = os.path.join(output_dir, "features")
+ os.makedirs(features_dir, exist_ok=True)
+ features_path = os.path.join(features_dir, f"{model_name}.npz")
+
+ # Try to load existing features first
+ if os.path.exists(features_path):
+ print(f"Loading existing features from {features_path}")
+ return self._load_saved_features(features_path)
+
+ print(f"Extracting new DINO features...")
+
+ # Initialize lazy DINO
+ dino = LazyLoadDino(
+ model_name=model_name, device=self.device, use_trt=self.use_trt
+ )
+
+ # Convert voxel centers to tensor coordinates
+ positions = torch.from_numpy(voxel_centers).float().to(self.device)
+ indices = ((positions + 0.5) * 64).long()
+ indices = torch.clamp(indices, 0, 63)
+
+ # Convert to int32 for sparse tensor coordinates
+ indices = indices.int()
+
+ # Create coordinates tensor (batch_idx, x, y, z)
+ batch_dim = torch.zeros(
+ (indices.shape[0], 1), dtype=torch.int32, device=self.device
+ )
+ coords = torch.cat([batch_dim, indices], dim=1)
+
+ # Process rendered images
+ data = []
+ renders_dir = os.path.join(output_dir, "renders")
+
+ for frame in frames_metadata:
+ image_path = os.path.join(renders_dir, frame["file_path"])
+
+ if not os.path.exists(image_path):
+ continue
+
+ # Load and preprocess image
+ image = Image.open(image_path)
+ image = image.resize((image_size, image_size), Image.Resampling.LANCZOS)
+ image = np.array(image).astype(np.float32) / 255
+
+ if image.shape[2] == 4:
+ image = image[:, :, :3] * image[:, :, 3:]
+ else:
+ image = image[:, :, :3]
+
+ image = torch.from_numpy(image).permute(2, 0, 1).float()
+
+ # Camera matrices
+ c2w = torch.tensor(frame["transform_matrix"], dtype=torch.float32)
+ c2w[:3, 1:3] *= -1
+ extrinsics = torch.inverse(c2w)
+ fov = frame["camera_angle_x"]
+ intrinsics = utils3d.torch.intrinsics_from_fov_xy(
+ torch.tensor(fov, dtype=torch.float32),
+ torch.tensor(fov, dtype=torch.float32),
+ )
+
+ data.append(
+ {"image": image, "extrinsics": extrinsics, "intrinsics": intrinsics}
+ )
+
+ if len(data) == 0:
+ raise ValueError("No valid rendered images found")
+
+ # Apply transforms and extract features
+ transform = dino.get_transform()
+ for datum in data:
+ datum["image"] = transform(datum["image"])
+
+ n_patch = dino.n_patch
+ patchtokens_lst = []
+ uv_lst = []
+
+ # Process in batches
+ for i in range(0, len(data), batch_size):
+ batch_data = data[i : i + batch_size]
+ bs = len(batch_data)
+
+ batch_images = torch.stack([d["image"] for d in batch_data]).to(self.device)
+ batch_extrinsics = torch.stack([d["extrinsics"] for d in batch_data]).to(
+ self.device
+ )
+ batch_intrinsics = torch.stack([d["intrinsics"] for d in batch_data]).to(
+ self.device
+ )
+
+ # Extract DINO features
+ model = dino.get_model()
+ with torch.inference_mode():
+ features = model(batch_images, is_training=True)
+
+ # Project voxels to image coordinates
+ uv = (
+ utils3d.torch.project_cv(positions, batch_extrinsics, batch_intrinsics)[
+ 0
+ ]
+ * 2
+ - 1
+ )
+
+ # Get patch tokens
+ patchtokens = (
+ features["x_prenorm"][:, model.num_register_tokens + 1 :]
+ .permute(0, 2, 1)
+ .reshape(bs, 1024, n_patch, n_patch)
+ )
+
+ patchtokens_lst.append(patchtokens)
+ uv_lst.append(uv)
+
+ # Concatenate all features
+ patchtokens = torch.cat(patchtokens_lst, dim=0)
+ uv = torch.cat(uv_lst, dim=0)
+
+ # Sample features at voxel locations
+ with torch.inference_mode():
+ sampled_features = (
+ F.grid_sample(
+ patchtokens,
+ uv.unsqueeze(1),
+ mode="bilinear",
+ align_corners=False,
+ )
+ .squeeze(2)
+ .permute(0, 2, 1)
+ )
+
+ # Aggregate features across views: mean in float32, then cast to float16 for storage
+ arr = sampled_features.cpu().numpy() # float32
+ arr_mean_f16 = np.mean(arr, axis=0).astype(np.float16)
+
+ # Save features to disk if requested
+ if save_features:
+ pack = {
+ "indices": indices.cpu().numpy().astype(np.uint8),
+ "patchtokens": arr_mean_f16,
+ }
+ np.savez_compressed(features_path, **pack)
+ print(f"Saved features to {features_path}")
+ print(f"Feature shape: {pack['patchtokens'].shape}")
+ print(f"Voxel indices shape: {pack['indices'].shape}")
+
+ # Convert to float32 tensor for inference
+ features = torch.from_numpy(arr_mean_f16).to(self.device).float()
+
+ return coords, features
+
+ def _load_saved_features(
+ self, features_path: str
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Load cached DINO features and coordinates from disk.
+
+ Reads previously extracted and saved DINO features from compressed
+ NPZ format. Reconstructs sparse tensor coordinates for use in model.
+
+ Args:
+ features_path: Path to compressed NPZ features file
+
+ Returns:
+ Tuple of (coordinates, features):
+ - coordinates: Sparse tensor coordinates (N, 4) [batch, x, y, z]
+ - features: DINO feature vectors (N, feature_dim)
+ """
+ data = np.load(features_path)
+
+ # Load indices and convert to coordinates
+ indices = torch.tensor(data["indices"]).int()
+ patchtokens = torch.tensor(data["patchtokens"]).float()
+
+ # Create coordinates tensor (batch_idx, x, y, z)
+ batch_dim = torch.zeros((indices.shape[0], 1), dtype=torch.int32)
+ coords = torch.cat([batch_dim, indices], dim=1)
+
+ # Move to device
+ coords = coords.to(self.device)
+ features = patchtokens.to(self.device)
+
+ print(f"Loaded features:")
+ print(f" Voxels: {indices.shape[0]}")
+ print(f" Feature dimension: {patchtokens.shape[1]}")
+ print(f" Voxel index range: {indices.min().item()} to {indices.max().item()}")
+
+ return coords, features
+
+ def _map_voxels_to_splats(
+ self,
+ gaussian_model: "Gaussian",
+ voxel_coords: np.ndarray,
+ voxel_results: Dict[str, np.ndarray],
+ ) -> Dict[str, np.ndarray]:
+ """Map voxel materials to closest splats"""
+ # Get splat positions
+ splat_positions = gaussian_model.get_xyz.detach().cpu().numpy()
+
+ # Find closest voxel for each splat
+
+ distances = cdist(splat_positions, voxel_coords)
+ closest_voxel_indices = np.argmin(distances, axis=1)
+
+ # Map materials to splats
+ splat_materials = {
+ "splat_positions": splat_positions,
+ "youngs_modulus": voxel_results["youngs_modulus"][closest_voxel_indices],
+ "poisson_ratio": voxel_results["poisson_ratio"][closest_voxel_indices],
+ "density": voxel_results["density"][closest_voxel_indices],
+ "closest_voxel_distance": distances[
+ np.arange(len(splat_positions)), closest_voxel_indices
+ ],
+ "num_splats": len(splat_positions),
+ "num_voxels": len(voxel_coords),
+ }
+
+ return splat_materials
+
+ def _install_blender(self):
+ """Install Blender 3.0.1 for mesh rendering if not already present."""
+ BLENDER_LINK = "https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
+ BLENDER_INSTALLATION_PATH = "/tmp"
+ BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
+
+ if not os.path.exists(BLENDER_PATH):
+ print("Installing Blender...")
+ os.system("sudo apt-get update")
+ os.system(
+ "sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
+ )
+ os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
+ os.system(
+ f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
+ )
+ print(f"โ Blender installed at: {BLENDER_PATH}")
+
+ return BLENDER_PATH
+
+ def _render_views_sequential(
+ self,
+ views: List[Dict],
+ mesh_path: str,
+ renders_dir: str,
+ image_size: int,
+ blender_path: str,
+ use_gpu: bool,
+ gpu_device: str,
+ ) -> List[Dict]:
+ """Render views sequentially using single Blender process."""
+ # Call Blender rendering script
+ args = [
+ blender_path,
+ "-b",
+ "-P",
+ os.path.join(
+ os.path.dirname(__file__),
+ "..",
+ "..",
+ "dataset_toolkits",
+ "blender_script",
+ "render.py",
+ ),
+ "--",
+ "--views",
+ json.dumps(views),
+ "--object",
+ os.path.abspath(os.path.expanduser(mesh_path)),
+ "--resolution",
+ str(image_size),
+ "--output_folder",
+ renders_dir,
+ "--engine",
+ "CYCLES",
+ "--save_mesh",
+ ]
+
+ # Add GPU rendering arguments
+ if use_gpu:
+ args.extend(["--use_gpu", "--gpu_device", gpu_device])
+ print(f"โ GPU rendering enabled: {gpu_device}")
+
+ if mesh_path.endswith(".blend"):
+ args.insert(1, mesh_path)
+
+ call(args, stdout=DEVNULL, stderr=DEVNULL)
+
+ # Load metadata
+ transforms_path = os.path.join(renders_dir, "transforms.json")
+ if os.path.exists(transforms_path):
+ with open(transforms_path, "r") as f:
+ transforms_data = json.load(f)
+ return transforms_data.get("frames", [])
+ else:
+ raise RuntimeError(
+ f"Blender did not create transforms.json at {transforms_path}"
+ )
+
+ def _render_views_parallel(
+ self,
+ views: List[Dict],
+ mesh_path: str,
+ renders_dir: str,
+ image_size: int,
+ blender_path: str,
+ use_gpu: bool,
+ gpu_device: str,
+ num_jobs: int,
+ ) -> List[Dict]:
+ """Render views in parallel using multiple Blender processes."""
+
+ # Split views into chunks for parallel processing
+ chunk_size = max(1, len(views) // num_jobs)
+ view_chunks = [
+ views[i : i + chunk_size] for i in range(0, len(views), chunk_size)
+ ]
+
+ print(
+ f"Splitting {len(views)} views into {len(view_chunks)} chunks (max {chunk_size} views per job)"
+ )
+
+ # Create temporary directories for each job
+ temp_dirs = []
+ for i in range(len(view_chunks)):
+ temp_dir = os.path.join(renders_dir, f"temp_job_{i}")
+ os.makedirs(temp_dir, exist_ok=True)
+ temp_dirs.append(temp_dir)
+
+ # Create worker arguments with proper view indexing
+ worker_args = []
+ view_index_offset = 0
+ for i, (chunk, temp_dir) in enumerate(zip(view_chunks, temp_dirs)):
+ worker_args.append(
+ {
+ "job_id": i,
+ "views": chunk,
+ "view_index_offset": view_index_offset,
+ "mesh_path": mesh_path,
+ "temp_dir": temp_dir,
+ "image_size": image_size,
+ "blender_path": blender_path,
+ "use_gpu": use_gpu,
+ "gpu_device": gpu_device,
+ "render_script": os.path.join(
+ os.path.dirname(__file__),
+ "..",
+ "..",
+ "dataset_toolkits",
+ "blender_script",
+ "render.py",
+ ),
+ }
+ )
+ view_index_offset += len(chunk)
+
+ # Run parallel rendering
+ with multiprocessing.Pool(processes=num_jobs) as pool:
+ results = pool.map(self._render_chunk_worker, worker_args)
+
+ # Merge results and rename files to avoid conflicts
+ all_frames = []
+ global_view_index = 0
+
+ for i, frames in enumerate(results):
+ if frames is None:
+ raise RuntimeError(f"Job {i} failed to render")
+
+ # Copy rendered images to main renders directory with correct global numbering
+ temp_dir = temp_dirs[i]
+ for j, frame in enumerate(frames):
+ # Original filename from job (e.g., "000.png", "001.png")
+ old_path = os.path.join(temp_dir, frame["file_path"])
+
+ # New filename with global indexing (e.g., "000.png", "038.png", "075.png")
+ new_filename = f"{global_view_index:03d}.png"
+ new_path = os.path.join(renders_dir, new_filename)
+
+ if os.path.exists(old_path):
+ os.rename(old_path, new_path)
+ # Update frame metadata with new filename
+ frame["file_path"] = new_filename
+ else:
+ print(f"Warning: Expected file not found: {old_path}")
+
+ global_view_index += 1
+
+ all_frames.extend(frames)
+
+ # Clean up temporary directory
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+ # Save merged transforms.json
+ transforms_data = {
+ "camera_angle_x": views[0]["fov"] if views else 0.0,
+ "frames": all_frames,
+ }
+
+ transforms_path = os.path.join(renders_dir, "transforms.json")
+ with open(transforms_path, "w") as f:
+ json.dump(transforms_data, f, indent=2)
+
+ print(
+ f"โ Merged {len(all_frames)} rendered views from {num_jobs} parallel jobs"
+ )
+ return all_frames
+
+ @staticmethod
+ def _render_chunk_worker(args):
+ """
+ Worker function for parallel mesh rendering using Blender.
+
+ Renders a subset of camera views in a separate Blender process as part
+ of parallel rendering pipeline. Each worker handles its assigned views
+ independently to improve throughput.
+
+ Args:
+ args: Dictionary containing worker configuration:
+ - job_id: Unique worker identifier for logging
+ - views: List of camera view parameters to render
+ - mesh_path: Path to input mesh file
+ - temp_dir: Temporary directory for this worker's output
+ - image_size: Target image resolution
+ - blender_path: Path to Blender executable
+ - use_gpu: Whether to use GPU acceleration
+ - gpu_device: GPU device type string
+ - render_script: Path to Blender rendering script
+
+ Returns:
+ List of frame metadata dictionaries for rendered views, or None on failure
+ """
+ job_id = args["job_id"]
+ views = args["views"]
+ mesh_path = args["mesh_path"]
+ temp_dir = args["temp_dir"]
+ image_size = args["image_size"]
+ blender_path = args["blender_path"]
+ use_gpu = args["use_gpu"]
+ gpu_device = args["gpu_device"]
+ render_script = args["render_script"]
+
+ print(f" Job {job_id}: Rendering {len(views)} views...")
+
+ # Prepare Blender arguments
+ blender_args = [
+ blender_path,
+ "-b",
+ "-P",
+ render_script,
+ "--",
+ "--views",
+ json.dumps(views),
+ "--object",
+ os.path.expanduser(mesh_path),
+ "--resolution",
+ str(image_size),
+ "--output_folder",
+ temp_dir,
+ "--engine",
+ "CYCLES",
+ ]
+
+ # Add GPU rendering arguments
+ if use_gpu:
+ blender_args.extend(["--use_gpu", "--gpu_device", gpu_device])
+
+ if mesh_path.endswith(".blend"):
+ blender_args.insert(1, mesh_path)
+
+ try:
+ # Run Blender with error output to help debug
+
+ process = Popen(blender_args, stdout=PIPE, stderr=PIPE)
+ stdout, stderr = process.communicate()
+
+ if process.returncode != 0:
+ print(
+ f" Job {job_id}: Blender error - returncode {process.returncode}"
+ )
+ if stderr:
+ print(f" Job {job_id}: stderr: {stderr.decode()[:200]}...")
+ return None
+
+ # Load and return metadata
+ transforms_path = os.path.join(temp_dir, "transforms.json")
+ if os.path.exists(transforms_path):
+ with open(transforms_path, "r") as f:
+ transforms_data = json.load(f)
+ frames = transforms_data.get("frames", [])
+ print(f" Job {job_id}: โ Completed {len(frames)} views")
+ return frames
+ else:
+ print(f" Job {job_id}: โ Failed - no transforms.json created")
+ print(
+ f" Job {job_id}: temp_dir contents: {os.listdir(temp_dir) if os.path.exists(temp_dir) else 'N/A'}"
+ )
+ return None
+
+ except Exception as e:
+ print(f" Job {job_id}: โ Failed with error: {e}")
+ return None
+
+ def render_mesh_views(
+ self,
+ mesh_path: str,
+ output_dir: str,
+ num_views: int = 150,
+ image_size: int = 518,
+ radius: float = 2.0,
+ fov: float = 40.0,
+ seed: Optional[int] = None,
+ blender_path: Optional[str] = None,
+ use_gpu: bool = True,
+ gpu_device: str = "OPTIX",
+ num_render_jobs: int = 1,
+ ) -> List[Dict]:
+ """
+ Render multiple views of mesh using Blender for photorealistic results.
+
+ Uses Blender's Cycles render engine to generate high-quality mesh renders
+ from sampled camera positions. Supports GPU acceleration and parallel
+ rendering for improved performance.
+
+ Args:
+ mesh_path: Path to mesh file (OBJ, PLY, STL, blend, etc.)
+ output_dir: Directory to save rendered images and metadata
+ num_views: Number of camera viewpoints to render
+ image_size: Square image resolution in pixels
+ radius: Camera distance from object center
+ fov: Field of view angle in degrees
+ seed: Random seed for reproducible camera sampling
+ blender_path: Path to Blender executable (uses BLENDER_BIN env var if None)
+ use_gpu: Whether to enable GPU acceleration (CUDA/OptiX/OpenCL)
+ gpu_device: GPU compute device type for Blender
+ num_render_jobs: Number of parallel Blender processes
+
+ Returns:
+ List of frame metadata dictionaries with camera poses and file paths
+
+ Raises:
+ EnvironmentError: If Blender executable not found
+ FileNotFoundError: If mesh file doesn't exist
+ RuntimeError: If rendering process fails
+ """
+ renders_dir = os.path.join(output_dir, "renders")
+ os.makedirs(renders_dir, exist_ok=True)
+
+ # Determine Blender path
+ if blender_path is not None:
+ BLENDER_PATH = blender_path
+ if not os.path.exists(BLENDER_PATH):
+ raise FileNotFoundError(
+ f"Blender executable not found at: {BLENDER_PATH}"
+ )
+ else:
+ # Try to get from environment variable
+ BLENDER_PATH = os.environ.get("BLENDER_BIN")
+ if BLENDER_PATH is None:
+ raise EnvironmentError(
+ "No Blender path provided and BLENDER_BIN environment variable not set. "
+ "Either provide blender_path parameter or set BLENDER_BIN environment variable. "
+ "You can install Blender using the install_env.sh script."
+ )
+ if not os.path.exists(BLENDER_PATH):
+ raise FileNotFoundError(
+ f"Blender executable not found at environment path: {BLENDER_PATH}. "
+ "Please check your BLENDER_BIN environment variable or reinstall Blender using install_env.sh"
+ )
+
+ print(f"Rendering {num_views} mesh views with Blender...")
+
+ # Sample camera views using shared utility
+ yaws, pitchs, radius_list, fov_list = self._sample_camera_views(
+ num_views, radius, fov, seed
+ )
+
+ # Convert to Blender format
+ views = [
+ {"yaw": y, "pitch": p, "radius": r, "fov": f / 180 * np.pi}
+ for y, p, r, f in zip(yaws, pitchs, radius_list, fov_list)
+ ]
+
+ if num_render_jobs == 1:
+ # Sequential rendering (original behavior)
+ frames_metadata = self._render_views_sequential(
+ views,
+ mesh_path,
+ renders_dir,
+ image_size,
+ BLENDER_PATH,
+ use_gpu,
+ gpu_device,
+ )
+ else:
+ # Parallel rendering
+ print(f"Using {num_render_jobs} parallel Blender processes...")
+ frames_metadata = self._render_views_parallel(
+ views,
+ mesh_path,
+ renders_dir,
+ image_size,
+ BLENDER_PATH,
+ use_gpu,
+ gpu_device,
+ num_render_jobs,
+ )
+
+ # Save metadata for pipeline compatibility
+ metadata_path = os.path.join(output_dir, "renders_metadata.json")
+ with open(metadata_path, "w") as f:
+ json.dump(frames_metadata, f, indent=2)
+
+ print(f"โ Rendered {len(frames_metadata)} views")
+ return frames_metadata
+
+ def render_views_replicator(
+ self,
+ asset_path: str,
+ output_dir: str,
+ num_views: int = 150,
+ image_size: int = 518,
+ radius: float = 2.0,
+ fov: float = 40.0,
+ seed: Optional[int] = None,
+ isaac_sim_path: Optional[str] = None,
+ render_mode: str = "path_tracing",
+ rtx_settings_override: Optional[Dict[str, Any]] = None,
+ ) -> List[Dict]:
+ """
+ Render multiple views using Isaac Sim Replicator (alternative to Blender).
+
+ Uses NVIDIA Isaac Sim's Replicator API for GPU-accelerated rendering with
+ RTX ray tracing or path tracing. Provides fast rendering with physically
+ accurate lighting and materials.
+
+ Args:
+ asset_path: Path to USD asset file (.usd, .usda, .usdc, .ply converted to USD)
+ output_dir: Directory to save rendered images and metadata
+ num_views: Number of camera viewpoints to render
+ image_size: Square image resolution in pixels
+ radius: Camera distance from object center
+ fov: Field of view angle in degrees
+ seed: Random seed for reproducible camera sampling
+ isaac_sim_path: Path to Isaac Sim executable (isaac-sim.sh).
+ If None, uses ISAAC_SIM_PATH environment variable.
+ render_mode: Rendering quality preset:
+ - "fast": Real-time ray tracing (faster, good quality)
+ - "path_tracing": Path tracing (slower, highest quality)
+ rtx_settings_override: Optional dict to override specific RTX settings.
+ Example: {"/rtx/pathtracing/spp": 512} for higher quality
+
+ Returns:
+ List of frame metadata dictionaries with camera poses and file paths
+
+ Raises:
+ EnvironmentError: If Isaac Sim path not provided and not in environment
+ FileNotFoundError: If Isaac Sim executable not found
+ RuntimeError: If rendering process fails
+
+ Example:
+ >>> # Using path tracing with default settings
+ >>> frames = model.render_views_replicator(
+ ... asset_path="model.usd",
+ ... output_dir="./renders",
+ ... num_views=150,
+ ... isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ ... render_mode="path_tracing"
+ ... )
+ >>>
+ >>> # Using fast mode with custom RTX settings
+ >>> frames = model.render_views_replicator(
+ ... asset_path="model.usd",
+ ... output_dir="./renders",
+ ... isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ ... render_mode="fast",
+ ... rtx_settings_override={
+ ... "/rtx/pathtracing/spp": 512, # Override samples
+ ... }
+ ... )
+ """
+
+ renders_dir = os.path.join(output_dir, "renders")
+ os.makedirs(renders_dir, exist_ok=True)
+
+ # Determine Isaac Sim path
+ if isaac_sim_path is not None:
+ ISAAC_SIM_PATH = isaac_sim_path
+ if not os.path.exists(os.path.expanduser(ISAAC_SIM_PATH)):
+ raise FileNotFoundError(
+ f"Isaac Sim executable not found at: {ISAAC_SIM_PATH}"
+ )
+ else:
+ # Try to get from environment variable
+ ISAAC_SIM_PATH = os.environ.get("ISAAC_SIM_PATH")
+ if ISAAC_SIM_PATH is None:
+ raise EnvironmentError(
+ "No Isaac Sim path provided and ISAAC_SIM_PATH environment variable not set. "
+ "Either provide isaac_sim_path parameter or set ISAAC_SIM_PATH environment variable. "
+ "Example: export ISAAC_SIM_PATH=~/isaac-sim/isaac-sim.sh"
+ )
+ if not os.path.exists(os.path.expanduser(ISAAC_SIM_PATH)):
+ raise FileNotFoundError(
+ f"Isaac Sim executable not found at environment path: {ISAAC_SIM_PATH}. "
+ "Please check your ISAAC_SIM_PATH environment variable."
+ )
+
+ print(f"Rendering {num_views} views with Isaac Sim Replicator...")
+ print(f"Render mode: {render_mode}")
+
+ # Sample camera views using shared utility
+ yaws, pitchs, radius_list, fov_list = self._sample_camera_views(
+ num_views, radius, fov, seed
+ )
+
+ # Call Replicator rendering
+ frames_metadata = render_with_replicator(
+ asset_path=asset_path,
+ output_dir=output_dir,
+ num_views=num_views,
+ yaws=yaws,
+ pitchs=pitchs,
+ radius_list=radius_list,
+ fov_list=fov_list,
+ isaac_sim_path=ISAAC_SIM_PATH,
+ resolution=(image_size, image_size),
+ render_mode=render_mode,
+ rtx_settings_override=rtx_settings_override,
+ light_intensity=1000.0,
+ normalize_object=True,
+ )
+
+ # Save metadata for pipeline compatibility
+ metadata_path = os.path.join(output_dir, "renders_metadata.json")
+ with open(metadata_path, "w") as f:
+ json.dump(frames_metadata, f, indent=2)
+
+ print(f"โ Rendered {len(frames_metadata)} views with Replicator")
+ return frames_metadata
+
+ def _voxelize_mesh(
+ self,
+ mesh_path: str,
+ output_dir: str,
+ voxel_size: float = 1 / 64,
+ max_voxels: Optional[int] = None,
+ ) -> np.ndarray:
+ """
+ Convert mesh geometry to discrete voxel representation.
+
+ Loads mesh file and generates voxel occupancy grid using trimesh
+ voxelization. Handles both single meshes and complex scenes with
+ multiple geometries. Normalizes to standard coordinate system.
+
+ Args:
+ mesh_path: Path to mesh file (supports OBJ, PLY, STL, etc.)
+ output_dir: Directory to save voxel PLY file
+ voxel_size: Voxel spacing in normalized coordinates (1/64 = 64ยณ grid)
+ max_voxels: Maximum voxels to generate; subsamples if exceeded
+
+ Returns:
+ Array of voxel center coordinates in world space (N, 3)
+ """
+ voxels_dir = os.path.join(output_dir, "voxels")
+ os.makedirs(voxels_dir, exist_ok=True)
+
+ print(f"Voxelizing mesh: {mesh_path}")
+
+ # Load and combine mesh geometry
+ mesh = trimesh.load(mesh_path)
+ if hasattr(mesh, "vertices") and hasattr(mesh, "faces"):
+ vertices, faces = mesh.vertices, mesh.faces
+ else:
+ # Mesh scene - combine all geometries
+ vertices_list, faces_list = [], []
+ vertex_offset = 0
+ for geometry in mesh.geometry.values():
+ if hasattr(geometry, "vertices") and hasattr(geometry, "faces"):
+ vertices_list.append(geometry.vertices)
+ faces_list.append(geometry.faces + vertex_offset)
+ vertex_offset += len(geometry.vertices)
+ if not vertices_list:
+ raise ValueError("No valid geometry found in mesh")
+ vertices, faces = np.vstack(vertices_list), np.vstack(faces_list)
+
+ # Step 1: Compute scale from original bbox
+ bbox_min, bbox_max = vertices.min(axis=0), vertices.max(axis=0)
+ bbox_size = (bbox_max - bbox_min).max()
+ blender_scale = 1 / bbox_size
+
+ # Step 2: Apply scale to vertices
+ vertices_scaled = vertices * blender_scale
+
+ # Step 3: Compute offset from SCALED bbox (this is what Blender does!)
+ bbox_min_scaled = vertices_scaled.min(axis=0)
+ bbox_max_scaled = vertices_scaled.max(axis=0)
+ blender_offset = -(bbox_min_scaled + bbox_max_scaled) / 2
+
+ # Step 4: Apply offset
+ vertices_normalized = vertices_scaled + blender_offset
+ vertices_normalized = np.clip(vertices_normalized, -0.5 + 1e-6, 0.5 - 1e-6)
+
+ # Now voxelize the normalized mesh
+ normalized_mesh = trimesh.Trimesh(vertices=vertices_normalized, faces=faces)
+ voxel_grid = normalized_mesh.voxelized(pitch=voxel_size).fill()
+ voxel_centers = voxel_grid.points
+
+ # Subsample if too many voxels
+ if max_voxels is not None and len(voxel_centers) > max_voxels:
+ print(f"Subsampling voxels: {len(voxel_centers):,} -> {max_voxels:,}")
+ np.random.seed(42) # For reproducibility
+ indices = np.random.choice(len(voxel_centers), max_voxels, replace=False)
+ voxel_centers = voxel_centers[indices]
+
+ indices = ((voxel_centers + 0.5) * 64).astype(np.int64)
+ indices = np.clip(indices, 0, 63)
+ discretized_positions = indices.astype(np.float32) / 64.0 - 0.5
+
+ voxel_centers = discretized_positions
+
+ # Save as PLY for compatibility with existing pipeline
+ voxel_path = os.path.join(voxels_dir, "voxels.ply")
+ self._save_voxels_ply(voxel_centers, voxel_path)
+
+ print(f"Generated {len(voxel_centers)} voxels")
+ return voxel_centers
+
+ @torch.inference_mode()
+ def get_mesh_materials(
+ self,
+ mesh_path: str,
+ output_dir: Optional[str] = None,
+ num_views: int = 150,
+ image_size: int = 518,
+ render_image_size: int = 512,
+ radius: float = 2.0,
+ fov: float = 40.0,
+ seed: Optional[int] = None,
+ voxel_size: float = 1 / 64,
+ max_voxels: Optional[int] = 32768,
+ blender_path: Optional[str] = None,
+ query_points: Union[str, np.ndarray, None] = "mesh_vertices",
+ use_gpu: bool = True,
+ gpu_device: str = "OPTIX",
+ num_render_jobs: int = 1,
+ dino_batch_size: int = 16,
+ return_original_scale: bool = False,
+ **kwargs: Any,
+ ) -> Dict[str, np.ndarray]:
+ """
+ High-level API for mesh material property inference.
+
+ Args:
+ mesh_path: Path to mesh file
+ output_dir: Directory for intermediate files
+ num_views: Camera views for rendering (default: 150, matches training data)
+ image_size: Target image size for DINO feature extraction. Rendered images are
+ resized to this resolution before processing (default: 518)
+ render_image_size: Resolution for Blender rendering. Set to 512 to match training
+ data pipeline where images were rendered at 512x512 then resized to 518x518
+ for DINO feature extraction (default: 512)
+ radius: Camera distance from object center (default: 2.0, matches training data)
+ fov: Field of view in degrees (default: 40.0, matches training data)
+ seed: Random seed for camera view sampling
+ voxel_size: Voxel size for discretization (default: 1/64)
+ max_voxels: Maximum voxels to generate
+ blender_path: Path to Blender executable. If None, uses fallback installation.
+ query_points: Where to evaluate material properties. Can be:
+ - "mesh_vertices" (default): Evaluate at mesh vertices
+ - "voxel_centers": Evaluate at voxel centers
+ - numpy array (N, 3): Custom 3D coordinates for evaluation
+ - None: Same as "voxel_centers"
+ use_gpu: Whether to use GPU acceleration for Blender rendering
+ gpu_device: GPU device type for Blender - "OPTIX", "CUDA", or "OPENCL"
+ num_render_jobs: Number of parallel Blender processes for rendering (default: 1)
+ dino_batch_size: Number of images to process simultaneously during DINO feature extraction (higher values use more GPU memory but may be faster)
+ return_original_scale: If True, return coordinates in original mesh scale instead of
+ normalized [-0.5, 0.5] space. Transformation parameters (center, scale) are included
+ in results for reference. (default: False)
+ **kwargs: Additional feature extraction arguments
+
+ Returns:
+ Dictionary containing material properties:
+ - 'youngs_modulus': Young's modulus values at query points (Pa)
+ - 'poisson_ratio': Poisson's ratio values at query points
+ - 'density': Density values at query points (kg/mยณ)
+ - 'query_coords_world': World coordinates of query points (normalized or original scale)
+ - 'voxel_coords_world': World coordinates of voxels (normalized or original scale)
+ - 'num_voxels': Number of voxels
+ - 'num_query_points': Number of query points (when applicable)
+ - 'transform_center': Center point used for normalization (when return_original_scale=True)
+ - 'transform_scale': Scale factor used for normalization (when return_original_scale=True)
+ """
+ if output_dir is None:
+ mesh_name = os.path.splitext(os.path.basename(mesh_path))[0]
+ output_dir = f"/tmp/Vomp_mesh_{mesh_name}"
+
+ print("=== Vomp: Mesh Material Estimation ===")
+
+ # Define built-in mesh rendering and voxelization functions
+ def mesh_render_func(mesh_path, output_dir, num_views, image_size, **kwargs):
+ return self.render_mesh_views(
+ mesh_path,
+ output_dir,
+ num_views,
+ render_image_size,
+ blender_path=blender_path,
+ use_gpu=use_gpu,
+ gpu_device=gpu_device,
+ num_render_jobs=num_render_jobs,
+ **kwargs,
+ )
+
+ def mesh_voxelize_func(mesh_path, output_dir, **kwargs):
+ return self._voxelize_mesh(
+ mesh_path, output_dir, voxel_size=voxel_size, max_voxels=max_voxels
+ )
+
+ # Step 1: Extract features using built-in mesh functions
+ print("Step 1: Extracting features...")
+
+ # Prepare rendering parameters for kwargs
+ render_kwargs = {
+ "radius": radius,
+ "fov": fov,
+ "seed": seed,
+ }
+
+ coords, features = self.get_features(
+ obj_3d=mesh_path,
+ render_func=mesh_render_func,
+ voxelize_func=mesh_voxelize_func,
+ num_views=num_views,
+ image_size=image_size,
+ output_dir=output_dir,
+ batch_size=dino_batch_size,
+ **render_kwargs,
+ **kwargs,
+ )
+
+ # Step 2: Run inference on the features
+ print("Step 2: Running material inference...")
+ predict_kwargs = {}
+ if max_voxels is not None:
+ predict_kwargs["max_voxels"] = max_voxels
+ voxel_results = self.predict_materials(coords, features, **predict_kwargs)
+
+ # Handle query_points parameter
+ if query_points == "mesh_vertices":
+ # Step 3: Evaluate materials at mesh vertices
+ print("Step 3: Evaluating materials at mesh vertices...")
+
+ # Load mesh to get vertices
+ mesh = trimesh.load(mesh_path)
+ if hasattr(mesh, "vertices"):
+ mesh_vertices = mesh.vertices
+ else:
+ # Mesh scene - combine all vertices
+ vertices_list = []
+ for geometry in mesh.geometry.values():
+ if hasattr(geometry, "vertices"):
+ vertices_list.append(geometry.vertices)
+ if not vertices_list:
+ raise ValueError("No vertices found in mesh")
+ mesh_vertices = np.vstack(vertices_list)
+
+ # Get transformation parameters
+ center, scale = get_mesh_transform_params(mesh_path)
+
+ # Normalize vertices to match model's coordinate system (same as _voxelize_mesh)
+ normalized_vertices = (mesh_vertices - center) / scale
+ normalized_vertices = np.clip(normalized_vertices, -0.5 + 1e-6, 0.5 - 1e-6)
+
+ # Create upsampler from voxel results
+ upsampler = MaterialUpsampler(
+ voxel_coords=voxel_results["voxel_coords_world"],
+ voxel_materials=np.column_stack(
+ [
+ voxel_results["youngs_modulus"],
+ voxel_results["poisson_ratio"],
+ voxel_results["density"],
+ ]
+ ),
+ )
+
+ # Interpolate to mesh vertices
+ vertex_materials, vertex_distances = upsampler.interpolate(
+ normalized_vertices
+ )
+
+ # Apply inverse transformation
+ if return_original_scale:
+ output_coords = denormalize_coords(normalized_vertices, center, scale)
+ output_voxel_coords = denormalize_coords(
+ voxel_results["voxel_coords_world"], center, scale
+ )
+ else:
+ output_coords = normalized_vertices
+ output_voxel_coords = voxel_results["voxel_coords_world"]
+
+ # Create final results
+ results = {
+ "youngs_modulus": vertex_materials[:, 0],
+ "poisson_ratio": vertex_materials[:, 1],
+ "density": vertex_materials[:, 2],
+ "query_coords_world": output_coords,
+ "query_distances": vertex_distances,
+ "voxel_coords_world": output_voxel_coords,
+ "num_voxels": voxel_results["num_voxels"],
+ "num_query_points": len(normalized_vertices),
+ }
+
+ # Add transformation parameters if in original scale
+ if return_original_scale:
+ results["transform_center"] = center
+ results["transform_scale"] = scale
+
+ print(
+ f"โ Evaluated materials at {len(normalized_vertices):,} mesh vertices"
+ )
+ elif query_points == "voxel_centers" or query_points is None:
+ # Return voxel-level results (original behavior)
+ results = voxel_results
+
+ # Apply inverse transformation
+ if return_original_scale:
+ # Get transformation parameters
+ center, scale = get_mesh_transform_params(mesh_path)
+
+ # Transform coordinates back to original scale
+ results["voxel_coords_world"] = denormalize_coords(
+ results["voxel_coords_world"], center, scale
+ )
+ results["transform_center"] = center
+ results["transform_scale"] = scale
+
+ elif isinstance(query_points, np.ndarray):
+ # Step 3: Evaluate materials at custom query points
+ print(
+ f"Step 3: Evaluating materials at {len(query_points)} custom query points..."
+ )
+
+ # Create upsampler from voxel results
+ upsampler = MaterialUpsampler(
+ voxel_coords=voxel_results["voxel_coords_world"],
+ voxel_materials=np.column_stack(
+ [
+ voxel_results["youngs_modulus"],
+ voxel_results["poisson_ratio"],
+ voxel_results["density"],
+ ]
+ ),
+ )
+
+ # Interpolate to custom query points
+ query_materials, query_distances = upsampler.interpolate(query_points)
+
+ # Apply inverse transformation
+ if return_original_scale:
+ # Get transformation parameters
+ center, scale = get_mesh_transform_params(mesh_path)
+
+ output_coords = denormalize_coords(query_points, center, scale)
+ output_voxel_coords = denormalize_coords(
+ voxel_results["voxel_coords_world"], center, scale
+ )
+ else:
+ output_coords = query_points
+ output_voxel_coords = voxel_results["voxel_coords_world"]
+
+ # Create final results
+ results = {
+ "youngs_modulus": query_materials[:, 0],
+ "poisson_ratio": query_materials[:, 1],
+ "density": query_materials[:, 2],
+ "query_coords_world": output_coords,
+ "query_distances": query_distances,
+ "voxel_coords_world": output_voxel_coords,
+ "num_voxels": voxel_results["num_voxels"],
+ "num_query_points": len(query_points),
+ }
+
+ # Add transformation parameters if in original scale
+ if return_original_scale:
+ results["transform_center"] = center
+ results["transform_scale"] = scale
+
+ print(f"โ Evaluated materials at {len(query_points):,} custom query points")
+ else:
+ raise ValueError(
+ f"Invalid query_points value: {query_points}. Must be 'mesh_vertices', 'voxel_centers', None, or numpy array."
+ )
+
+ print("โ Material estimation complete!")
+ return results
+
+ @torch.inference_mode()
+ def get_usd_materials(
+ self,
+ usd_path: str,
+ mesh_path: Optional[str] = None,
+ output_dir: Optional[str] = None,
+ num_views: int = 150,
+ image_size: int = 518,
+ render_image_size: int = 512,
+ radius: float = 2.0,
+ fov: float = 40.0,
+ seed: Optional[int] = None,
+ voxel_size: float = 1 / 64,
+ max_voxels: Optional[int] = 32768,
+ isaac_sim_path: Optional[str] = None,
+ render_mode: str = "path_tracing",
+ rtx_settings_override: Optional[Dict[str, Any]] = None,
+ query_points: Union[str, np.ndarray, None] = "voxel_centers",
+ dino_batch_size: int = 16,
+ use_simready_usd_format: bool = False,
+ blender_path: Optional[str] = None,
+ gpu_device: str = "OPTIX",
+ num_render_jobs: int = 1,
+ return_original_scale: bool = False,
+ **kwargs: Any,
+ ) -> Dict[str, np.ndarray]:
+ """
+ High-level API for USD asset material property inference.
+
+ USD files can come in many different formats with varying internal structures,
+ materials, and organization. In the general case, users should provide both:
+ 1. The USD file for rendering
+ 2. An extracted mesh file (OBJ/PLY/STL) for voxelization
+
+ However, if your USD file is in the SimReady format (like the USD files in our
+ dataset), you can set use_simready_usd_format=True.
+ This will automatically:
+ - Extract mesh geometry, materials, and UV coordinates from the USD
+ - Convert to OBJ format matching the dataset preprocessing pipeline
+ - Render with Blender using the exact same pipeline used to create training data
+
+ Args:
+ usd_path: Path to USD asset file (.usd, .usda, .usdc)
+ mesh_path: Path to mesh file (OBJ, PLY, STL, etc.) for voxelization.
+ If None and use_simready_usd_format=True, voxelizes the converted OBJ.
+ Required if use_simready_usd_format=False (for Replicator mode).
+ output_dir: Directory for intermediate files
+ num_views: Camera views for rendering (default: 150, matches training data)
+ image_size: Target image size for DINO feature extraction. Rendered images are
+ resized to this resolution before processing (default: 518)
+ render_image_size: Resolution for rendering. Set to 512 to match training
+ data pipeline where images were rendered at 512x512 then resized to 518x518
+ for DINO feature extraction (default: 512)
+ radius: Camera distance from object center (default: 2.0, matches training data)
+ fov: Field of view in degrees (default: 40.0, matches training data)
+ seed: Random seed for camera view sampling
+ voxel_size: Voxel size for discretization
+ max_voxels: Maximum voxels to generate
+ isaac_sim_path: Path to Isaac Sim executable (isaac-sim.sh).
+ Only used if use_simready_usd_format=False.
+ If None, uses ISAAC_SIM_PATH environment variable.
+ render_mode: Rendering quality preset (Replicator only):
+ - "fast": Real-time ray tracing (faster, good quality)
+ - "path_tracing": Path tracing (slower, highest quality)
+ rtx_settings_override: Optional dict to override specific RTX settings (Replicator only).
+ Example: {"/rtx/pathtracing/spp": 512} for higher quality
+ query_points: Where to evaluate material properties. Can be:
+ - "voxel_centers" (default): Evaluate at voxel centers
+ - numpy array (N, 3): Custom 3D coordinates for evaluation
+ - None: Same as "voxel_centers"
+ dino_batch_size: Number of images to process simultaneously during
+ DINO feature extraction
+ use_simready_usd_format: If True, treats the USD as SimReady format
+ (like assets in datasets/raw/simready). Automatically converts USD to OBJ
+ with materials/textures and renders with Blender, matching the dataset
+ preprocessing pipeline. If False (default), uses Isaac Sim Replicator.
+ blender_path: Path to Blender executable (only used if use_simready_usd_format=True).
+ If None, attempts to auto-detect.
+ gpu_device: GPU rendering device for Blender (only used if use_simready_usd_format=True).
+ Options: "OPTIX" (default), "CUDA", "METAL", "NONE"
+ num_render_jobs: Number of parallel Blender render jobs (only used if use_simready_usd_format=True).
+ return_original_scale: If True, return coordinates in original mesh scale instead of
+ normalized [-0.5, 0.5] space. Transformation parameters (center, scale) are included
+ in results for reference. (default: False)
+ **kwargs: Additional feature extraction arguments
+
+ Returns:
+ Dictionary containing material properties:
+ - 'youngs_modulus': Young's modulus values (Pa)
+ - 'poisson_ratio': Poisson's ratio values
+ - 'density': Density values (kg/mยณ)
+ - 'voxel_coords_world': World coordinates of voxels (normalized or original scale)
+ - 'query_coords_world': World coordinates of query points (when using custom points)
+ - 'num_voxels': Number of voxels
+ - 'num_query_points': Number of query points (when applicable)
+ - 'transform_center': Center point used for normalization (when return_original_scale=True)
+ - 'transform_scale': Scale factor used for normalization (when return_original_scale=True)
+
+ Example:
+ >>> # Replicator mode (default) - requires separate mesh file
+ >>> model = Vomp.from_checkpoint(config_path)
+ >>> results = model.get_usd_materials(
+ ... usd_path="model.usd",
+ ... mesh_path="model.ply",
+ ... isaac_sim_path="~/isaac-sim/isaac-sim.sh",
+ ... render_mode="path_tracing"
+ ... )
+ >>>
+ >>> # SimReady USD format
+ >>> results = model.get_usd_materials(
+ ... usd_path="datasets/raw/simready/common_assets/props/dellwood_ottoman/dellwood_ottoman_inst_base.usd",
+ ... use_simready_usd_format=True,
+ ... blender_path="/usr/bin/blender",
+ ... seed=42
+ ... )
+ """
+ if output_dir is None:
+ asset_name = os.path.splitext(os.path.basename(usd_path))[0]
+ output_dir = f"/tmp/Vomp_usd_{asset_name}"
+
+ # Select rendering backend
+ if use_simready_usd_format:
+
+ # Convert USD to OBJ in a temporary directory
+ temp_obj_path, temp_dir = convert_usd_to_obj(usd_path)
+
+ try:
+ print(f"โ USD converted to OBJ: {temp_obj_path}")
+
+ results = self.get_mesh_materials(
+ mesh_path=temp_obj_path,
+ output_dir=output_dir,
+ num_views=num_views,
+ image_size=image_size,
+ radius=radius,
+ fov=fov,
+ seed=seed,
+ voxel_size=voxel_size,
+ max_voxels=max_voxels,
+ blender_path=blender_path,
+ gpu_device=gpu_device,
+ num_render_jobs=num_render_jobs,
+ query_points=query_points,
+ dino_batch_size=dino_batch_size,
+ return_original_scale=return_original_scale,
+ **kwargs,
+ )
+
+ return results
+
+ finally:
+ # Clean up temporary directory
+ if os.path.exists(temp_dir):
+ print(f"Cleaning up temporary directory: {temp_dir}")
+ shutil.rmtree(temp_dir)
+
+ else:
+
+ if mesh_path is None:
+ raise ValueError(
+ "mesh_path is required when use_blender_render=False. "
+ "Provide a separate mesh file for voxelization, or set use_blender_render=True "
+ "to automatically convert USD to OBJ."
+ )
+
+ # Define rendering function using Replicator
+ def usd_render_func(usd_path, output_dir, num_views, image_size, **kwargs):
+ # Use render_image_size for actual rendering (default 512 to match training data)
+ return self.render_views_replicator(
+ asset_path=usd_path,
+ output_dir=output_dir,
+ num_views=num_views,
+ image_size=render_image_size,
+ isaac_sim_path=isaac_sim_path,
+ render_mode=render_mode,
+ rtx_settings_override=rtx_settings_override,
+ **kwargs,
+ )
+
+ # Define voxelization function using the provided mesh
+ def usd_voxelize_func(usd_path, output_dir, **kwargs):
+ # Use the separate mesh file for voxelization
+ # Note: The mesh may not be normalized, so _voxelize_mesh will handle that
+ return self._voxelize_mesh(
+ mesh_path, output_dir, voxel_size=voxel_size, max_voxels=max_voxels
+ )
+
+ # Step 1: Extract features
+ print("Step 1: Extracting features...")
+
+ # Prepare rendering parameters for kwargs
+ render_kwargs = {
+ "radius": radius,
+ "fov": fov,
+ "seed": seed,
+ }
+
+ coords, features = self.get_features(
+ obj_3d=usd_path,
+ render_func=usd_render_func,
+ voxelize_func=usd_voxelize_func,
+ num_views=num_views,
+ image_size=image_size,
+ output_dir=output_dir,
+ batch_size=dino_batch_size,
+ **render_kwargs,
+ **kwargs,
+ )
+
+ # Step 2: Run inference on the features
+ print("Step 2: Running material inference...")
+ predict_kwargs = {}
+ if max_voxels is not None:
+ predict_kwargs["max_voxels"] = max_voxels
+ voxel_results = self.predict_materials(coords, features, **predict_kwargs)
+
+ # Handle query_points parameter
+ if query_points == "voxel_centers" or query_points is None:
+ # Return voxel-level results
+ results = voxel_results
+
+ # Apply inverse transformation
+ if return_original_scale:
+ # Get transformation parameters
+ center, scale = get_mesh_transform_params(mesh_path)
+
+ # Transform coordinates back to original scale
+ results["voxel_coords_world"] = denormalize_coords(
+ results["voxel_coords_world"], center, scale
+ )
+ results["transform_center"] = center
+ results["transform_scale"] = scale
+
+ elif isinstance(query_points, np.ndarray):
+ # Step 3: Evaluate materials at custom query points
+ print(
+ f"Step 3: Evaluating materials at {len(query_points)} custom query points..."
+ )
+
+ # Create upsampler from voxel results
+ upsampler = MaterialUpsampler(
+ voxel_coords=voxel_results["voxel_coords_world"],
+ voxel_materials=np.column_stack(
+ [
+ voxel_results["youngs_modulus"],
+ voxel_results["poisson_ratio"],
+ voxel_results["density"],
+ ]
+ ),
+ )
+
+ # Interpolate to custom query points
+ query_materials, query_distances = upsampler.interpolate(query_points)
+
+ # Apply inverse transformation
+ if return_original_scale:
+ # Get transformation parameters
+ center, scale = get_mesh_transform_params(mesh_path)
+
+ output_coords = denormalize_coords(query_points, center, scale)
+ output_voxel_coords = denormalize_coords(
+ voxel_results["voxel_coords_world"], center, scale
+ )
+ else:
+ output_coords = query_points
+ output_voxel_coords = voxel_results["voxel_coords_world"]
+
+ # Create final results
+ results = {
+ "youngs_modulus": query_materials[:, 0],
+ "poisson_ratio": query_materials[:, 1],
+ "density": query_materials[:, 2],
+ "query_coords_world": output_coords,
+ "query_distances": query_distances,
+ "voxel_coords_world": output_voxel_coords,
+ "num_voxels": voxel_results["num_voxels"],
+ "num_query_points": len(query_points),
+ }
+
+ # Add transformation parameters if in original scale
+ if return_original_scale:
+ results["transform_center"] = center
+ results["transform_scale"] = scale
+
+ print(
+ f"โ Evaluated materials at {len(query_points):,} custom query points"
+ )
+ else:
+ raise ValueError(
+ f"Invalid query_points value: {query_points}. Must be 'voxel_centers', None, or numpy array."
+ )
+
+ print("โ Material estimation complete!")
+ return results
diff --git a/deps/vomp/vomp/models/__init__.py b/deps/vomp/vomp/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff20c1c2c7e967f67553dd2de2287797523d4742
--- /dev/null
+++ b/deps/vomp/vomp/models/__init__.py
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .geometry_encoder import GeometryEncoder
+from .material_vae.beta_tc import TripletVAE
+
+__all__ = ["GeometryEncoder", "TripletVAE"]
diff --git a/deps/vomp/vomp/models/geometry_encoder.py b/deps/vomp/vomp/models/geometry_encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee243bc2769fd539ae18c7c99cd06260ef39e9e4
--- /dev/null
+++ b/deps/vomp/vomp/models/geometry_encoder.py
@@ -0,0 +1,162 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from ..modules import sparse as sp
+from .structured_latent_vae.base import SparseTransformerBase
+from .sparse_elastic_mixin import SparseTransformerElasticMixin
+
+
+class GeometryEncoder(SparseTransformerBase):
+ def __init__(
+ self,
+ resolution: int,
+ in_channels: int,
+ model_channels: int,
+ latent_channels: int,
+ num_blocks: int,
+ num_heads: Optional[int] = None,
+ num_head_channels: Optional[int] = 64,
+ mlp_ratio: float = 4,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "swin",
+ window_size: int = 8,
+ pe_mode: Literal["ape", "rope"] = "ape",
+ use_fp16: bool = False,
+ use_checkpoint: bool = False,
+ qk_rms_norm: bool = False,
+ ):
+ super().__init__(
+ in_channels=in_channels,
+ model_channels=model_channels,
+ num_blocks=num_blocks,
+ num_heads=num_heads,
+ num_head_channels=num_head_channels,
+ mlp_ratio=mlp_ratio,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ pe_mode=pe_mode,
+ use_fp16=use_fp16,
+ use_checkpoint=use_checkpoint,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.resolution = resolution
+ self.out_layer = sp.SparseLinear(model_channels, 2 * latent_channels)
+
+ self.initialize_weights()
+ if use_fp16:
+ self.convert_to_fp16()
+
+ def initialize_weights(self) -> None:
+ super().initialize_weights()
+ # Zero-out output layers:
+ nn.init.constant_(self.out_layer.weight, 0)
+ nn.init.constant_(self.out_layer.bias, 0)
+
+ def forward(self, x: sp.SparseTensor, sample_posterior=True, return_raw=False):
+ h = super().forward(x)
+ h = h.type(x.dtype)
+ h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
+ h = self.out_layer(h)
+
+ # Sample from the posterior distribution
+ mean, logvar = h.feats.chunk(2, dim=-1)
+ if sample_posterior:
+ std = torch.exp(0.5 * logvar)
+ z = mean + std * torch.randn_like(std)
+ else:
+ z = mean
+ z = h.replace(z)
+
+ if return_raw:
+ return z, mean, logvar
+ else:
+ return z
+
+
+class ElasticGeometryEncoder(SparseTransformerElasticMixin, GeometryEncoder):
+ """
+ SLat VAE encoder with elastic memory management.
+ Used for training with low VRAM.
+ """
+
+
+class SLatVoxelDecoder(SparseTransformerBase):
+ def __init__(
+ self,
+ resolution: int,
+ model_channels: int,
+ latent_channels: int,
+ num_blocks: int,
+ out_channels: int,
+ num_heads: Optional[int] = None,
+ num_head_channels: Optional[int] = 64,
+ mlp_ratio: float = 4,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "swin",
+ window_size: int = 8,
+ pe_mode: Literal["ape", "rope"] = "ape",
+ use_fp16: bool = False,
+ use_checkpoint: bool = False,
+ qk_rms_norm: bool = False,
+ ):
+ super().__init__(
+ in_channels=latent_channels,
+ model_channels=model_channels,
+ num_blocks=num_blocks,
+ num_heads=num_heads,
+ num_head_channels=num_head_channels,
+ mlp_ratio=mlp_ratio,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ pe_mode=pe_mode,
+ use_fp16=use_fp16,
+ use_checkpoint=use_checkpoint,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.resolution = resolution
+ self.out_channels = out_channels
+ self.out_layer = sp.SparseLinear(model_channels, out_channels)
+
+ self.initialize_weights()
+ if use_fp16:
+ self.convert_to_fp16()
+
+ def initialize_weights(self) -> None:
+ super().initialize_weights()
+ # Zero-out output layers:
+ nn.init.constant_(self.out_layer.weight, 0)
+ nn.init.constant_(self.out_layer.bias, 0)
+
+ def forward(self, x: sp.SparseTensor) -> sp.SparseTensor:
+ h = super().forward(x)
+ h = h.type(x.dtype)
+ h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
+ h = self.out_layer(h)
+ return h
+
+
+class ElasticSLatVoxelDecoder(SparseTransformerElasticMixin, SLatVoxelDecoder):
+ """
+ SLat VAE voxel decoder with elastic memory management.
+ Used for training with low VRAM.
+ """
+
+ pass
diff --git a/deps/vomp/vomp/models/material_vae/__init__.py b/deps/vomp/vomp/models/material_vae/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/models/material_vae/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/models/material_vae/beta_tc.py b/deps/vomp/vomp/models/material_vae/beta_tc.py
new file mode 100644
index 0000000000000000000000000000000000000000..3408a970465a1f651a32399b8d8e0d7424137245
--- /dev/null
+++ b/deps/vomp/vomp/models/material_vae/beta_tc.py
@@ -0,0 +1,405 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Tuple, Dict, Any
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from torch.distributions import Normal
+import math
+
+
+class ResidualBlock(nn.Module):
+ """Optimized residual block for VAE architectures.
+
+ Uses bottleneck design with pre-normalization and SiLU activation.
+ Follows VAE best practices for stable training.
+ """
+
+ def __init__(self, width: int, p_drop: float = 0.0):
+ super().__init__()
+ # Bottleneck design: width -> width//2 -> width
+ # This reduces parameters while maintaining expressiveness
+ bottleneck_width = max(width // 2, 16) # Minimum bottleneck of 16
+
+ # Pre-normalization design (LayerNorm before activation)
+ self.norm1 = nn.LayerNorm(width)
+ self.norm2 = nn.LayerNorm(bottleneck_width)
+
+ # Bottleneck layers with SiLU activation (works well for VAEs)
+ self.down = nn.Linear(width, bottleneck_width)
+ self.up = nn.Linear(bottleneck_width, width)
+
+ # Initialize weights properly for VAE training
+ self._init_weights()
+
+ def _init_weights(self):
+ """Initialize weights for stable VAE training."""
+ # Xavier/Glorot initialization for linear layers
+ nn.init.xavier_uniform_(self.down.weight)
+ nn.init.xavier_uniform_(self.up.weight)
+
+ # Small bias initialization
+ nn.init.zeros_(self.down.bias)
+ nn.init.zeros_(self.up.bias)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ # Pre-norm residual connection
+ residual = x
+
+ # First path: norm -> activation -> linear
+ x = self.norm1(x)
+ x = F.silu(x) # SiLU activation (x * sigmoid(x))
+ x = self.down(x)
+
+ # Second path: norm -> activation -> linear
+ x = self.norm2(x)
+ x = F.silu(x)
+ x = self.up(x)
+
+ # Residual connection
+ return residual + x
+
+
+class RadialFlow(nn.Module):
+
+ def __init__(self, z_dim: int):
+ super().__init__()
+ self.z0 = nn.Parameter(torch.randn(1, z_dim))
+ self.log_alpha = nn.Parameter(torch.zeros(1))
+ self.beta = nn.Parameter(torch.zeros(1))
+
+ def forward(
+ self, z: torch.Tensor, log_det_j: torch.Tensor
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ alpha = F.softplus(self.log_alpha) + 1e-5 # Add small constant for stability
+ beta = -alpha + F.softplus(self.beta)
+
+ diff = z - self.z0
+ r = diff.norm(dim=1, keepdim=True) + 1e-8 # Avoid division by zero
+ h = 1.0 / (alpha + r)
+
+ z_new = z + beta * h * diff
+
+ # Compute log determinant with clamping for stability
+ bh = beta * h.squeeze(-1) # (B,)
+ bh_clamped = torch.clamp(bh, min=-0.999, max=0.999)
+ term1 = (z.size(1) - 1) * torch.log1p(bh_clamped)
+ term2 = torch.log1p(bh_clamped - beta * (h.squeeze(-1) ** 2) * r.squeeze(-1))
+ log_det = term1 + term2
+
+ return z_new, log_det_j + log_det
+
+
+class TripletVAE(nn.Module):
+
+ def __init__(
+ self,
+ width: int = 64,
+ depth: int = 2,
+ z_dim: int = 4,
+ p_drop: float = 0.0, # Kept for interface compatibility, but not used
+ use_learned_variances: bool = True,
+ init_logvar: float = -2.0, # Initialize to small variances (exp(-2) = 0.135)
+ min_logvar: float = -10.0, # Minimum log-variance for stability
+ max_logvar: float = 2.0, # Maximum log-variance to prevent explosion
+ use_additional_losses: bool = True, # Use MSE + NLL for better reconstruction
+ **kwargs,
+ ) -> None:
+ super().__init__()
+ self.width, self.z_dim = width, z_dim
+ self.use_learned_variances = use_learned_variances
+ self.init_logvar = init_logvar
+ self.min_logvar = min_logvar
+ self.max_logvar = max_logvar
+ self.use_additional_losses = use_additional_losses
+
+ # โ Encoder (optimized for VAE training)
+ self.enc_in = nn.Linear(3, width) # Removed dropout
+ self.encoder = nn.Sequential(
+ *[ResidualBlock(width, p_drop=0.0) for _ in range(depth)]
+ )
+
+ # Latent space heads
+ self.mu_head = nn.Linear(width, z_dim)
+ self.logvar_head = nn.Linear(width, z_dim)
+
+ # โ One radial flow (optional normalizing flow for better posterior)
+ self.use_flow = kwargs.pop("use_flow", True)
+ # Radial flow can be disabled via config (use_flow=False)
+ self.flow = RadialFlow(z_dim) if self.use_flow else nn.Identity()
+
+ # โ Decoder (symmetric to encoder)
+ self.dec_in = nn.Linear(z_dim, width) # Removed dropout
+ self.decoder = nn.Sequential(
+ *[ResidualBlock(width, p_drop=0.0) for _ in range(depth)]
+ )
+
+ # Output heads for material properties
+ self.out_E = nn.Linear(width, 1) # Young's modulus
+ self.out_nu = nn.Linear(width, 1) # Poisson's ratio
+ self.out_rho = nn.Linear(width, 1) # Density
+
+ if self.use_learned_variances:
+ # Learned log-variances with proper initialization
+ self.out_E_logvar = nn.Linear(width, 1)
+ self.out_nu_logvar = nn.Linear(width, 1)
+ self.out_rho_logvar = nn.Linear(width, 1)
+
+ # Initialize variance heads to small values
+ self._init_variance_heads()
+ else:
+ # Fixed small variances for stable training
+ self.register_buffer("fixed_E_logvar", torch.tensor([init_logvar]))
+ self.register_buffer("fixed_nu_logvar", torch.tensor([init_logvar]))
+ self.register_buffer("fixed_rho_logvar", torch.tensor([init_logvar]))
+
+ # Isotropic Normal prior
+ self.register_buffer("prior_mu", torch.zeros(z_dim))
+ self.register_buffer("prior_std", torch.ones(z_dim))
+
+ # Initialize all weights properly
+ self._init_weights()
+
+ def _init_weights(self):
+ """Initialize weights following VAE best practices."""
+ # Initialize input/output layers
+ nn.init.xavier_uniform_(self.enc_in.weight)
+ nn.init.zeros_(self.enc_in.bias)
+
+ nn.init.xavier_uniform_(self.dec_in.weight)
+ nn.init.zeros_(self.dec_in.bias)
+
+ # Initialize latent space heads
+ nn.init.xavier_uniform_(self.mu_head.weight)
+ nn.init.zeros_(self.mu_head.bias)
+
+ # Initialize logvar head to predict small initial variances
+ nn.init.xavier_uniform_(self.logvar_head.weight, gain=0.1)
+ nn.init.constant_(self.logvar_head.bias, self.init_logvar)
+
+ # Initialize output heads
+ for head in [self.out_E, self.out_nu, self.out_rho]:
+ nn.init.xavier_uniform_(head.weight)
+ nn.init.zeros_(head.bias)
+
+ def _init_variance_heads(self):
+ """Initialize variance heads to predict small, consistent variances."""
+ for head in [self.out_E_logvar, self.out_nu_logvar, self.out_rho_logvar]:
+ # Initialize weights to small values for stable variance prediction
+ nn.init.xavier_uniform_(head.weight, gain=0.1)
+ # Initialize bias to init_logvar for small initial variance
+ nn.init.constant_(head.bias, self.init_logvar)
+
+ @staticmethod
+ def posterior_log_probs(
+ z: torch.Tensor, mu: torch.Tensor, logvar: torch.Tensor
+ ) -> torch.Tensor:
+ """Element-wise log q(z|x) under diagonal Gaussian."""
+ return Normal(mu, torch.exp(0.5 * logvar)).log_prob(z).sum(dim=-1)
+
+ def encode(
+ self, x: torch.Tensor, sample: bool = True
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """
+ Encode input to latent space parameters mu and logvar.
+
+ Args:
+ x: Input tensor [batch_size, 3]
+ sample: Whether to sample from the distribution or return the mean
+
+ Returns:
+ z: Sampled latent vector if sample=True, else mu
+ mu: Mean of the latent distribution
+ logvar: Log variance of the latent distribution
+ """
+ # Input projection with activation
+ h = F.silu(self.enc_in(x))
+
+ # Pass through residual blocks
+ h = self.encoder(h)
+
+ # Get latent parameters
+ mu, logvar = self.mu_head(h), self.logvar_head(h)
+
+ # Ensure numerical stability (prevent extreme logvar values)
+ logvar = torch.clamp(logvar, min=-15.0, max=15.0)
+
+ z = self._reparameterise(mu, logvar) if sample else mu
+ return z, mu, logvar
+
+ def decode(self, z: torch.Tensor):
+ """
+ Decode latent vector to output distribution parameters.
+
+ Args:
+ z: Latent vector [batch_size, z_dim]
+
+ Returns:
+ Tuple of (mean, logvar) pairs for each output: E, nu, rho
+ """
+ # Latent projection with activation
+ h = F.silu(self.dec_in(z))
+
+ # Pass through residual blocks
+ h = self.decoder(h)
+
+ # Get output means
+ E_mu = self.out_E(h)
+ nu_mu = self.out_nu(h)
+ rho_mu = self.out_rho(h)
+
+ if self.use_learned_variances:
+ # Use learned log-variances with proper clamping
+ E_logvar = torch.clamp(
+ self.out_E_logvar(h), self.min_logvar, self.max_logvar
+ )
+ nu_logvar = torch.clamp(
+ self.out_nu_logvar(h), self.min_logvar, self.max_logvar
+ )
+ rho_logvar = torch.clamp(
+ self.out_rho_logvar(h), self.min_logvar, self.max_logvar
+ )
+ else:
+ # Use fixed small variances
+ batch_size = z.size(0)
+ E_logvar = self.fixed_E_logvar.expand(batch_size, 1)
+ nu_logvar = self.fixed_nu_logvar.expand(batch_size, 1)
+ rho_logvar = self.fixed_rho_logvar.expand(batch_size, 1)
+
+ return (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar)
+
+ def forward(
+ self, x: torch.Tensor
+ ) -> Tuple[torch.Tensor, torch.Tensor, Dict[str, torch.Tensor]]:
+ """Return improved recon loss, total KL and a dict including sampled `z` for TC loss."""
+
+ # Encode & sample
+ z, mu, logvar = self.encode(x, sample=True)
+ log_q = self.posterior_log_probs(z, mu, logvar)
+
+ # Optional radial flow refinement
+ if self.use_flow:
+ log_det = torch.zeros_like(log_q)
+ z, log_det = self.flow(z, log_det)
+ log_q = log_q - log_det # q(z|x) after flow
+
+ # Decode & recon loss in transformed space
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = self.decode(z)
+
+ # Stack means for reconstruction output
+ x_mu = torch.stack(
+ [E_mu.squeeze(-1), nu_mu.squeeze(-1), rho_mu.squeeze(-1)], dim=-1
+ )
+
+ # Compute reconstruction loss
+ if self.use_additional_losses:
+ # 1. MSE loss for direct reconstruction (helps with mean prediction)
+ mse_loss = F.mse_loss(x_mu, x, reduction="mean")
+
+ if self.use_learned_variances:
+ # 2. NLL loss (helps with uncertainty estimation) - only when we have learned variances
+ E_std = torch.exp(0.5 * E_logvar)
+ nu_std = torch.exp(0.5 * nu_logvar)
+ rho_std = torch.exp(0.5 * rho_logvar)
+
+ nll_E = -Normal(E_mu, E_std).log_prob(x[..., 0]).mean()
+ nll_nu = -Normal(nu_mu, nu_std).log_prob(x[..., 1]).mean()
+ nll_rho = -Normal(rho_mu, rho_std).log_prob(x[..., 2]).mean()
+ nll_loss = nll_E + nll_nu + nll_rho
+
+ # Combine losses (MSE is main driver, NLL for uncertainty)
+ recon_loss = mse_loss + 0.1 * nll_loss
+
+ # Store individual components for logging
+ details_extra = {
+ "mse_loss": mse_loss.detach(),
+ "nll_loss": nll_loss.detach(),
+ "E_std_mean": E_std.mean().detach(),
+ "nu_std_mean": nu_std.mean().detach(),
+ "rho_std_mean": rho_std.mean().detach(),
+ }
+ else:
+ # Only MSE loss when using fixed variances
+ recon_loss = mse_loss
+
+ # Store individual components for logging
+ details_extra = {
+ "mse_loss": mse_loss.detach(),
+ "nll_loss": torch.tensor(0.0).detach(), # No NLL component
+ "E_std_mean": (
+ torch.exp(0.5 * torch.tensor(self.init_logvar)).detach()
+ ), # Fixed std
+ "nu_std_mean": (
+ torch.exp(0.5 * torch.tensor(self.init_logvar)).detach()
+ ), # Fixed std
+ "rho_std_mean": (
+ torch.exp(0.5 * torch.tensor(self.init_logvar)).detach()
+ ), # Fixed std
+ }
+ else:
+ # Original NLL-only loss
+ E_std = torch.exp(0.5 * E_logvar)
+ nu_std = torch.exp(0.5 * nu_logvar)
+ rho_std = torch.exp(0.5 * rho_logvar)
+
+ recon = -Normal(E_mu, E_std).log_prob(x[..., 0])
+ recon += -Normal(nu_mu, nu_std).log_prob(x[..., 1])
+ recon += -Normal(rho_mu, rho_std).log_prob(x[..., 2])
+ recon_loss = recon.mean()
+ details_extra = {}
+
+ # Prior logโprob
+ log_p = Normal(self.prior_mu, self.prior_std).log_prob(z).sum(dim=-1)
+
+ kl_total = log_q - log_p
+ details = {
+ "recon": recon_loss.detach(),
+ "kl_total": kl_total.detach(),
+ "z": z.detach(),
+ "mu": mu.detach(),
+ "logvar": logvar.detach(),
+ "log_q": log_q.detach(),
+ "x_mu": x_mu.detach(),
+ **details_extra,
+ }
+ return recon_loss, kl_total.mean(), details
+
+ def sample_prior(self, n: int) -> torch.Tensor:
+ """
+ Return `n ร z_dim` latent vectors drawn from the
+ isotropic unitโGaussian prior (โผ N(0,โฏI)).
+ """
+ return self.prior_mu + self.prior_std * torch.randn(
+ n, self.z_dim, device=self.prior_mu.device
+ )
+
+ # sample from prior then decode to transformed scalar means
+ def sample(self, n: int) -> torch.Tensor:
+ with torch.no_grad():
+ z = self.prior_mu + self.prior_std * torch.randn(
+ n, self.z_dim, device=self.prior_mu.device
+ )
+ (E_mu, _), (nu_mu, _), (rho_mu, _) = self.decode(z)
+ return torch.stack(
+ [E_mu.squeeze(-1), nu_mu.squeeze(-1), rho_mu.squeeze(-1)], dim=-1
+ )
+
+ # reparametrization
+ @staticmethod
+ def _reparameterise(mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor:
+ std = torch.exp(0.5 * logvar)
+ eps = torch.randn_like(std)
+ return mu + eps * std
diff --git a/deps/vomp/vomp/models/material_vae/standard_vae.py b/deps/vomp/vomp/models/material_vae/standard_vae.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc47d7f563a4d259f827bccff4a8ca923530f6d4
--- /dev/null
+++ b/deps/vomp/vomp/models/material_vae/standard_vae.py
@@ -0,0 +1,432 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Tuple, Dict
+import torch
+import torch.nn as nn
+from torch.distributions import Normal
+import torch.nn.functional as F
+
+
+class ResidualBlock(nn.Module):
+ """Optimized residual block for VAE architectures.
+
+ Uses bottleneck design with pre-normalization and SiLU activation.
+ Follows VAE best practices for stable training.
+ """
+
+ def __init__(self, width: int, p_drop: float = 0.0):
+ super().__init__()
+ # Bottleneck design: width -> width//2 -> width
+ # This reduces parameters while maintaining expressiveness
+ bottleneck_width = max(width // 2, 16) # Minimum bottleneck of 16
+
+ # Pre-normalization design (LayerNorm before activation)
+ self.norm1 = nn.LayerNorm(width)
+ self.norm2 = nn.LayerNorm(bottleneck_width)
+
+ # Bottleneck layers with SiLU activation (works well for VAEs)
+ self.down = nn.Linear(width, bottleneck_width)
+ self.up = nn.Linear(bottleneck_width, width)
+
+ # Initialize weights properly for VAE training
+ self._init_weights()
+
+ def _init_weights(self):
+ """Initialize weights for stable VAE training."""
+ # Xavier/Glorot initialization for linear layers
+ nn.init.xavier_uniform_(self.down.weight)
+ nn.init.xavier_uniform_(self.up.weight)
+
+ # Small bias initialization
+ nn.init.zeros_(self.down.bias)
+ nn.init.zeros_(self.up.bias)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ # Pre-norm residual connection
+ residual = x
+
+ # First path: norm -> activation -> linear
+ x = self.norm1(x)
+ x = F.silu(x) # SiLU activation (x * sigmoid(x))
+ x = self.down(x)
+
+ # Second path: norm -> activation -> linear
+ x = self.norm2(x)
+ x = F.silu(x)
+ x = self.up(x)
+
+ # Residual connection
+ return residual + x
+
+
+class RadialFlow(nn.Module):
+ """Radial normalizing flow layer."""
+
+ def __init__(self, z_dim: int):
+ super().__init__()
+ self.z0 = nn.Parameter(torch.randn(1, z_dim))
+ self.log_alpha = nn.Parameter(torch.zeros(1))
+ self.beta = nn.Parameter(torch.zeros(1))
+
+ def forward(
+ self, z: torch.Tensor, log_det_j: torch.Tensor
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ alpha = F.softplus(self.log_alpha) + 1e-5 # Add small constant for stability
+ beta = -alpha + F.softplus(self.beta)
+
+ diff = z - self.z0
+ r = diff.norm(dim=1, keepdim=True) + 1e-8 # Avoid division by zero
+ h = 1.0 / (alpha + r)
+
+ z_new = z + beta * h * diff
+
+ # Compute log determinant with clamping for stability
+ bh = beta * h.squeeze(-1) # (B,)
+ bh_clamped = torch.clamp(bh, min=-0.999, max=0.999)
+ term1 = (z.size(1) - 1) * torch.log1p(bh_clamped)
+ term2 = torch.log1p(bh_clamped - beta * (h.squeeze(-1) ** 2) * r.squeeze(-1))
+ log_det = term1 + term2
+
+ return z_new, log_det_j + log_det
+
+
+class StandardVAE(nn.Module):
+ def __init__(
+ self,
+ width: int = 64,
+ depth: int = 2,
+ z_dim: int = 4,
+ p_drop: float = 0.0, # Kept for interface compatibility, but not used
+ use_learned_variances: bool = True,
+ init_logvar: float = -2.0, # Initialize to small variances (exp(-2) = 0.135)
+ min_logvar: float = -10.0, # Minimum log-variance for stability
+ max_logvar: float = 2.0, # Maximum log-variance to prevent explosion
+ use_additional_losses: bool = True, # Use MSE + NLL for better reconstruction
+ **kwargs,
+ ) -> None:
+ super().__init__()
+ self.width, self.z_dim = width, z_dim
+ self.use_learned_variances = use_learned_variances
+ self.init_logvar = init_logvar
+ self.min_logvar = min_logvar
+ self.max_logvar = max_logvar
+ self.use_additional_losses = use_additional_losses
+
+ # โ Encoder (optimized for VAE training)
+ self.enc_in = nn.Linear(3, width) # Removed dropout
+ self.encoder = nn.Sequential(
+ *[ResidualBlock(width, p_drop=0.0) for _ in range(depth)]
+ )
+
+ # Latent space heads
+ self.mu_head = nn.Linear(width, z_dim)
+ self.logvar_head = nn.Linear(width, z_dim)
+
+ # โ Optional normalizing flow for better posterior
+ self.use_flow = kwargs.pop("use_flow", True)
+ self.flow = RadialFlow(z_dim) if self.use_flow else nn.Identity()
+
+ # โ Decoder (symmetric to encoder)
+ self.dec_in = nn.Linear(z_dim, width) # Removed dropout
+ self.decoder = nn.Sequential(
+ *[ResidualBlock(width, p_drop=0.0) for _ in range(depth)]
+ )
+
+ # Output heads for material properties
+ self.out_E = nn.Linear(width, 1) # Young's modulus
+ self.out_nu = nn.Linear(width, 1) # Poisson's ratio
+ self.out_rho = nn.Linear(width, 1) # Density
+
+ if self.use_learned_variances:
+ # Learned log-variances with proper initialization
+ self.out_E_logvar = nn.Linear(width, 1)
+ self.out_nu_logvar = nn.Linear(width, 1)
+ self.out_rho_logvar = nn.Linear(width, 1)
+
+ # Initialize variance heads to small values
+ self._init_variance_heads()
+ else:
+ # Fixed small variances for stable training
+ self.register_buffer("fixed_E_logvar", torch.tensor([init_logvar]))
+ self.register_buffer("fixed_nu_logvar", torch.tensor([init_logvar]))
+ self.register_buffer("fixed_rho_logvar", torch.tensor([init_logvar]))
+
+ # Isotropic Normal prior
+ self.register_buffer("prior_mu", torch.zeros(z_dim))
+ self.register_buffer("prior_std", torch.ones(z_dim))
+
+ # Initialize all weights properly
+ self._init_weights()
+
+ @staticmethod
+ def posterior_log_probs(
+ z: torch.Tensor, mu: torch.Tensor, logvar: torch.Tensor
+ ) -> torch.Tensor:
+ """Element-wise log q(z|x) under diagonal Gaussian."""
+ return Normal(mu, torch.exp(0.5 * logvar)).log_prob(z).sum(dim=-1)
+
+ def _init_weights(self):
+ """Initialize weights following VAE best practices."""
+ # Initialize input/output layers
+ nn.init.xavier_uniform_(self.enc_in.weight)
+ nn.init.zeros_(self.enc_in.bias)
+
+ nn.init.xavier_uniform_(self.dec_in.weight)
+ nn.init.zeros_(self.dec_in.bias)
+
+ # Initialize latent space heads
+ nn.init.xavier_uniform_(self.mu_head.weight)
+ nn.init.zeros_(self.mu_head.bias)
+
+ # Initialize logvar head to predict small initial variances
+ nn.init.xavier_uniform_(self.logvar_head.weight, gain=0.1)
+ nn.init.constant_(self.logvar_head.bias, self.init_logvar)
+
+ # Initialize output heads
+ for head in [self.out_E, self.out_nu, self.out_rho]:
+ nn.init.xavier_uniform_(head.weight)
+ nn.init.zeros_(head.bias)
+
+ def _init_variance_heads(self):
+ """Initialize variance heads to predict small, consistent variances."""
+ for head in [self.out_E_logvar, self.out_nu_logvar, self.out_rho_logvar]:
+ # Initialize weights to small values for stable variance prediction
+ nn.init.xavier_uniform_(head.weight, gain=0.1)
+ # Initialize bias to init_logvar for small initial variance
+ nn.init.constant_(head.bias, self.init_logvar)
+
+ def encode(
+ self, x: torch.Tensor, sample: bool = True
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """
+ Encode input to latent space parameters mu and logvar.
+
+ Args:
+ x: Input tensor [batch_size, 3]
+ sample: Whether to sample from the distribution or return the mean
+
+ Returns:
+ z: Sampled latent vector if sample=True, else mu
+ mu: Mean of the latent distribution
+ logvar: Log variance of the latent distribution
+ """
+ # Input projection with activation
+ h = F.silu(self.enc_in(x))
+
+ # Pass through residual blocks
+ h = self.encoder(h)
+
+ # Get latent parameters
+ mu, logvar = self.mu_head(h), self.logvar_head(h)
+
+ # Ensure numerical stability (prevent extreme logvar values)
+ logvar = torch.clamp(logvar, min=-15.0, max=15.0)
+
+ z = self._reparameterize(mu, logvar) if sample else mu
+ return z, mu, logvar
+
+ def decode(self, z: torch.Tensor) -> Tuple[Tuple[torch.Tensor, torch.Tensor], ...]:
+ """
+ Decode latent vector to output distribution parameters.
+
+ Args:
+ z: Latent vector [batch_size, z_dim]
+
+ Returns:
+ Tuple of (mean, logvar) pairs for each output: E, nu, rho
+ """
+ # Latent projection with activation
+ h = F.silu(self.dec_in(z))
+
+ # Pass through residual blocks
+ h = self.decoder(h)
+
+ # Get output means
+ E_mu = self.out_E(h)
+ nu_mu = self.out_nu(h)
+ rho_mu = self.out_rho(h)
+
+ if self.use_learned_variances:
+ # Use learned log-variances with proper clamping
+ E_logvar = torch.clamp(
+ self.out_E_logvar(h), self.min_logvar, self.max_logvar
+ )
+ nu_logvar = torch.clamp(
+ self.out_nu_logvar(h), self.min_logvar, self.max_logvar
+ )
+ rho_logvar = torch.clamp(
+ self.out_rho_logvar(h), self.min_logvar, self.max_logvar
+ )
+ else:
+ # Use fixed small variances
+ batch_size = z.size(0)
+ E_logvar = self.fixed_E_logvar.expand(batch_size, 1)
+ nu_logvar = self.fixed_nu_logvar.expand(batch_size, 1)
+ rho_logvar = self.fixed_rho_logvar.expand(batch_size, 1)
+
+ return (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar)
+
+ def forward(
+ self, x: torch.Tensor
+ ) -> Tuple[torch.Tensor, torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Forward pass through the VAE with improved reconstruction loss.
+
+ Args:
+ x: Input tensor [batch_size, 3] (standardized material properties)
+
+ Returns:
+ recon_loss: Total reconstruction loss
+ kl_loss: KL divergence loss
+ details: Dictionary with detailed information about the forward pass
+ """
+ # Encode & sample
+ z, mu, logvar = self.encode(x, sample=True)
+ log_q = self.posterior_log_probs(z, mu, logvar)
+
+ # Optional radial flow refinement
+ if self.use_flow:
+ log_det = torch.zeros_like(log_q)
+ z, log_det = self.flow(z, log_det)
+ log_q = log_q - log_det # q(z|x) after flow
+
+ # Decode & compute reconstruction loss
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = self.decode(z)
+
+ # Stack means for reconstruction output
+ x_mu = torch.stack(
+ [E_mu.squeeze(-1), nu_mu.squeeze(-1), rho_mu.squeeze(-1)], dim=-1
+ )
+
+ # Compute reconstruction loss
+ recon_loss = 0.0
+
+ if self.use_additional_losses:
+ # 1. MSE loss for direct reconstruction (helps with mean prediction)
+ mse_loss = F.mse_loss(x_mu, x, reduction="mean")
+
+ if self.use_learned_variances:
+ # 2. NLL loss (helps with uncertainty estimation) - only when we have learned variances
+ E_std = torch.exp(0.5 * E_logvar)
+ nu_std = torch.exp(0.5 * nu_logvar)
+ rho_std = torch.exp(0.5 * rho_logvar)
+
+ nll_E = -Normal(E_mu, E_std).log_prob(x[..., 0]).mean()
+ nll_nu = -Normal(nu_mu, nu_std).log_prob(x[..., 1]).mean()
+ nll_rho = -Normal(rho_mu, rho_std).log_prob(x[..., 2]).mean()
+ nll_loss = nll_E + nll_nu + nll_rho
+
+ # Combine losses (MSE is main driver, NLL for uncertainty)
+ recon_loss = mse_loss + 0.1 * nll_loss
+
+ # Store individual components for logging
+ details_extra = {
+ "mse_loss": mse_loss.detach(),
+ "nll_loss": nll_loss.detach(),
+ "E_std_mean": E_std.mean().detach(),
+ "nu_std_mean": nu_std.mean().detach(),
+ "rho_std_mean": rho_std.mean().detach(),
+ }
+ else:
+ # Only MSE loss when using fixed variances
+ recon_loss = mse_loss
+
+ # Store individual components for logging
+ details_extra = {
+ "mse_loss": mse_loss.detach(),
+ "nll_loss": torch.tensor(0.0).detach(), # No NLL component
+ "E_std_mean": (
+ torch.exp(0.5 * torch.tensor(self.init_logvar)).detach()
+ ), # Fixed std
+ "nu_std_mean": (
+ torch.exp(0.5 * torch.tensor(self.init_logvar)).detach()
+ ), # Fixed std
+ "rho_std_mean": (
+ torch.exp(0.5 * torch.tensor(self.init_logvar)).detach()
+ ), # Fixed std
+ }
+ else:
+ # Original NLL-only loss
+ E_std = torch.exp(0.5 * E_logvar)
+ nu_std = torch.exp(0.5 * nu_logvar)
+ rho_std = torch.exp(0.5 * rho_logvar)
+
+ recon = -Normal(E_mu, E_std).log_prob(x[..., 0])
+ recon += -Normal(nu_mu, nu_std).log_prob(x[..., 1])
+ recon += -Normal(rho_mu, rho_std).log_prob(x[..., 2])
+ recon_loss = recon.mean()
+ details_extra = {}
+
+ # KL divergence: analytical formula for KL(q(z|x) || p(z))
+ kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)
+ kl_loss = kl.mean()
+
+ details = {
+ "recon": recon_loss.detach(),
+ "kl": kl_loss.detach(),
+ "z": z.detach(),
+ "mu": mu.detach(),
+ "logvar": logvar.detach(),
+ "x_mu": x_mu.detach(),
+ **details_extra,
+ }
+
+ return recon_loss, kl_loss, details
+
+ def sample_prior(self, n: int) -> torch.Tensor:
+ """
+ Sample from the prior distribution.
+
+ Args:
+ n: Number of samples
+
+ Returns:
+ Samples from the prior [n, z_dim]
+ """
+ return torch.randn(n, self.z_dim, device=self.prior_mu.device)
+
+ def sample(self, n: int) -> torch.Tensor:
+ """
+ Sample from the model by sampling from the prior and decoding.
+
+ Args:
+ n: Number of samples
+
+ Returns:
+ Generated samples [n, 3]
+ """
+ with torch.no_grad():
+ z = self.sample_prior(n)
+ (E_mu, _), (nu_mu, _), (rho_mu, _) = self.decode(z)
+ return torch.stack(
+ [E_mu.squeeze(-1), nu_mu.squeeze(-1), rho_mu.squeeze(-1)], dim=-1
+ )
+
+ @staticmethod
+ def _reparameterize(mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor:
+ """
+ Reparameterization trick to sample from posterior.
+
+ Args:
+ mu: Mean of the posterior
+ logvar: Log variance of the posterior
+
+ Returns:
+ Sampled latent vector
+ """
+ std = torch.exp(0.5 * logvar)
+ eps = torch.randn_like(std)
+ return mu + eps * std
diff --git a/deps/vomp/vomp/models/sparse_elastic_mixin.py b/deps/vomp/vomp/models/sparse_elastic_mixin.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b3bccd449515bbf0b8419b2ee5a6581ff84af94
--- /dev/null
+++ b/deps/vomp/vomp/models/sparse_elastic_mixin.py
@@ -0,0 +1,41 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from contextlib import contextmanager
+from typing import *
+import math
+from ..modules import sparse as sp
+from ..utils.elastic_utils import ElasticModuleMixin
+
+
+class SparseTransformerElasticMixin(ElasticModuleMixin):
+ def _get_input_size(self, x: sp.SparseTensor, *args, **kwargs):
+ return x.feats.shape[0]
+
+ @contextmanager
+ def with_mem_ratio(self, mem_ratio=1.0):
+ if mem_ratio == 1.0:
+ yield 1.0
+ return
+ num_blocks = len(self.blocks)
+ num_checkpoint_blocks = min(
+ math.ceil((1 - mem_ratio) * num_blocks) + 1, num_blocks
+ )
+ exact_mem_ratio = 1 - (num_checkpoint_blocks - 1) / num_blocks
+ for i in range(num_blocks):
+ self.blocks[i].use_checkpoint = i < num_checkpoint_blocks
+ yield exact_mem_ratio
+ for i in range(num_blocks):
+ self.blocks[i].use_checkpoint = False
diff --git a/deps/vomp/vomp/models/structured_latent_vae/__init__.py b/deps/vomp/vomp/models/structured_latent_vae/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/models/structured_latent_vae/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/models/structured_latent_vae/base.py b/deps/vomp/vomp/models/structured_latent_vae/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..000e0724f554c6e33d0abdaa152f548b1c9524a2
--- /dev/null
+++ b/deps/vomp/vomp/models/structured_latent_vae/base.py
@@ -0,0 +1,148 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from ...modules.utils import convert_module_to_f16, convert_module_to_f32
+from ...modules import sparse as sp
+from ...modules.transformer import AbsolutePositionEmbedder
+from ...modules.sparse.transformer import SparseTransformerBlock
+
+
+def block_attn_config(self):
+ """
+ Return the attention configuration of the model.
+ """
+ for i in range(self.num_blocks):
+ if self.attn_mode == "shift_window":
+ yield "serialized", self.window_size, 0, (
+ 16 * (i % 2),
+ ) * 3, sp.SerializeMode.Z_ORDER
+ elif self.attn_mode == "shift_sequence":
+ yield "serialized", self.window_size, self.window_size // 2 * (i % 2), (
+ 0,
+ 0,
+ 0,
+ ), sp.SerializeMode.Z_ORDER
+ elif self.attn_mode == "shift_order":
+ yield "serialized", self.window_size, 0, (0, 0, 0), sp.SerializeModes[i % 4]
+ elif self.attn_mode == "full":
+ yield "full", None, None, None, None
+ elif self.attn_mode == "swin":
+ yield "windowed", self.window_size, None, self.window_size // 2 * (
+ i % 2
+ ), None
+
+
+class SparseTransformerBase(nn.Module):
+ """
+ Sparse Transformer without output layers.
+ Serve as the base class for encoder and decoder.
+ """
+
+ def __init__(
+ self,
+ in_channels: int,
+ model_channels: int,
+ num_blocks: int,
+ num_heads: Optional[int] = None,
+ num_head_channels: Optional[int] = 64,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "full",
+ window_size: Optional[int] = None,
+ pe_mode: Literal["ape", "rope"] = "ape",
+ use_fp16: bool = False,
+ use_checkpoint: bool = False,
+ qk_rms_norm: bool = False,
+ ):
+ super().__init__()
+ self.in_channels = in_channels
+ self.model_channels = model_channels
+ self.num_blocks = num_blocks
+ self.window_size = window_size
+ self.num_heads = num_heads or model_channels // num_head_channels
+ self.mlp_ratio = mlp_ratio
+ self.attn_mode = attn_mode
+ self.pe_mode = pe_mode
+ self.use_fp16 = use_fp16
+ self.use_checkpoint = use_checkpoint
+ self.qk_rms_norm = qk_rms_norm
+ self.dtype = torch.float16 if use_fp16 else torch.float32
+
+ if pe_mode == "ape":
+ self.pos_embedder = AbsolutePositionEmbedder(model_channels)
+
+ self.input_layer = sp.SparseLinear(in_channels, model_channels)
+ self.blocks = nn.ModuleList(
+ [
+ SparseTransformerBlock(
+ model_channels,
+ num_heads=self.num_heads,
+ mlp_ratio=self.mlp_ratio,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_sequence=shift_sequence,
+ shift_window=shift_window,
+ serialize_mode=serialize_mode,
+ use_checkpoint=self.use_checkpoint,
+ use_rope=(pe_mode == "rope"),
+ qk_rms_norm=self.qk_rms_norm,
+ )
+ for attn_mode, window_size, shift_sequence, shift_window, serialize_mode in block_attn_config(
+ self
+ )
+ ]
+ )
+
+ @property
+ def device(self) -> torch.device:
+ """
+ Return the device of the model.
+ """
+ return next(self.parameters()).device
+
+ def convert_to_fp16(self) -> None:
+ """
+ Convert the torso of the model to float16.
+ """
+ self.blocks.apply(convert_module_to_f16)
+
+ def convert_to_fp32(self) -> None:
+ """
+ Convert the torso of the model to float32.
+ """
+ self.blocks.apply(convert_module_to_f32)
+
+ def initialize_weights(self) -> None:
+ # Initialize transformer layers:
+ def _basic_init(module):
+ if isinstance(module, nn.Linear):
+ torch.nn.init.xavier_uniform_(module.weight)
+ if module.bias is not None:
+ nn.init.constant_(module.bias, 0)
+
+ self.apply(_basic_init)
+
+ def forward(self, x: sp.SparseTensor) -> sp.SparseTensor:
+ h = self.input_layer(x)
+ if self.pe_mode == "ape":
+ h = h + self.pos_embedder(x.coords[:, 1:])
+ h = h.type(self.dtype)
+ for block in self.blocks:
+ h = block(h)
+ return h
diff --git a/deps/vomp/vomp/models/structured_latent_vae/decoder_material.py b/deps/vomp/vomp/models/structured_latent_vae/decoder_material.py
new file mode 100644
index 0000000000000000000000000000000000000000..d09155fcfdfdd312ce0ccb61804dfc24291cf152
--- /dev/null
+++ b/deps/vomp/vomp/models/structured_latent_vae/decoder_material.py
@@ -0,0 +1,243 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from ...modules import sparse as sp
+from .base import SparseTransformerBase
+from ..sparse_elastic_mixin import SparseTransformerElasticMixin
+
+
+class MaterialProperties:
+ """
+ Container for material properties predictions.
+
+ Args:
+ youngs_modulus: Young's modulus values per voxel
+ poissons_ratio: Poisson's ratio values per voxel
+ density: Density values per voxel
+ coords: The coordinates of the voxels in the grid
+ """
+
+ def __init__(
+ self,
+ youngs_modulus: torch.Tensor,
+ poissons_ratio: torch.Tensor,
+ density: torch.Tensor,
+ coords: torch.Tensor,
+ ):
+ self.youngs_modulus = youngs_modulus
+ self.poissons_ratio = poissons_ratio
+ self.density = density
+ self.coords = coords
+
+ @property
+ def device(self):
+ return self.youngs_modulus.device
+
+
+class MaterialClassification:
+ """
+ Container for material classification predictions.
+
+ Args:
+ material_logits: Logits for material class prediction per voxel
+ coords: The coordinates of the voxels in the grid
+ """
+
+ def __init__(
+ self,
+ material_logits: torch.Tensor,
+ coords: torch.Tensor,
+ ):
+ self.material_logits = material_logits
+ self.coords = coords
+
+ @property
+ def device(self):
+ return self.material_logits.device
+
+ @property
+ def material_classes(self):
+ return torch.argmax(self.material_logits, dim=1)
+
+ @property
+ def material_probs(self):
+ return F.softmax(self.material_logits, dim=1)
+
+
+class SLatMaterialDecoder(SparseTransformerBase):
+ def __init__(
+ self,
+ resolution: int,
+ model_channels: int,
+ latent_channels: int,
+ num_blocks: int,
+ num_heads: Optional[int] = None,
+ num_head_channels: Optional[int] = 64,
+ mlp_ratio: float = 4,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "swin",
+ window_size: int = 8,
+ pe_mode: Literal["ape", "rope"] = "ape",
+ use_fp16: bool = False,
+ use_checkpoint: bool = False,
+ qk_rms_norm: bool = False,
+ num_classes: int = 18, # Number of material classes for classification mode
+ mode: Literal["regression", "classification"] = "regression",
+ ):
+ print("Creating SLatMaterialDecoder with:")
+ print(f" - Resolution: {resolution}")
+ print(f" - Model channels: {model_channels}")
+ print(f" - Latent channels: {latent_channels}")
+ print(f" - Num blocks: {num_blocks}")
+ print(f" - Attention mode: {attn_mode}")
+ print(f" - Number of material classes: {num_classes}")
+
+ super().__init__(
+ in_channels=latent_channels,
+ model_channels=model_channels,
+ num_blocks=num_blocks,
+ num_heads=num_heads,
+ num_head_channels=num_head_channels,
+ mlp_ratio=mlp_ratio,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ pe_mode=pe_mode,
+ use_fp16=use_fp16,
+ use_checkpoint=use_checkpoint,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.resolution = resolution
+ self.num_classes = num_classes
+ self.mode = mode
+
+ if self.mode == "regression":
+ # Output layer for regression mode (material properties)
+ self.out_channels = 3
+ self.out_layer = sp.SparseLinear(model_channels, self.out_channels)
+ else:
+ # Output layer for classification mode (material class)
+ self.classification_layer = sp.SparseLinear(model_channels, num_classes)
+
+ self.initialize_weights()
+ if use_fp16:
+ self.convert_to_fp16()
+ print(" - Using FP16 precision")
+ print("Decoder created for material properties prediction.")
+
+ def initialize_weights(self) -> None:
+ super().initialize_weights()
+
+ if self.mode == "regression":
+ nn.init.xavier_uniform_(self.out_layer.weight)
+ nn.init.constant_(self.out_layer.bias, 0)
+ else:
+ nn.init.xavier_uniform_(self.classification_layer.weight)
+ nn.init.constant_(self.classification_layer.bias, 0)
+
+ def to_material_properties(self, x: sp.SparseTensor) -> List[MaterialProperties]:
+ """
+ Convert a batch of network outputs to material properties.
+
+ Args:
+ x: The [N x * x C] sparse tensor output by the network.
+
+ Returns:
+ list of MaterialProperties objects for each item in the batch
+ """
+ ret = []
+ for i in range(x.shape[0]):
+ feats = x.feats[x.layout[i]]
+ coords = x.coords[x.layout[i]][:, 1:] # remove batch dimension
+
+ # Raw outputs - no activation functions
+ # The model learns to predict in the normalized transformed space
+ youngs_modulus = feats[:, 0]
+ poissons_ratio = feats[:, 1]
+ density = feats[:, 2]
+
+ representation = MaterialProperties(
+ youngs_modulus=youngs_modulus,
+ poissons_ratio=poissons_ratio,
+ density=density,
+ coords=coords,
+ )
+ ret.append(representation)
+
+ return ret
+
+ def to_material_classification(
+ self, x: sp.SparseTensor
+ ) -> List[MaterialClassification]:
+ """
+ Convert a batch of network outputs to material classification.
+
+ Args:
+ x: The [N x * x C] sparse tensor output by the network.
+
+ Returns:
+ list of MaterialClassification objects for each item in the batch
+ """
+ ret = []
+ for i in range(x.shape[0]):
+ feats = x.feats[x.layout[i]]
+ coords = x.coords[x.layout[i]][:, 1:] # remove batch dimension
+
+ # Material classification logits
+ material_logits = feats # Already the logits, no activation
+
+ representation = MaterialClassification(
+ material_logits=material_logits,
+ coords=coords,
+ )
+ ret.append(representation)
+
+ return ret
+
+ def forward(
+ self, x: sp.SparseTensor
+ ) -> Union[List[MaterialProperties], List[MaterialClassification]]:
+ """
+ Forward pass through the decoder.
+
+ Args:
+ x: The input sparse tensor.
+
+ Returns:
+ List of either MaterialProperties or MaterialClassification objects, depending on mode.
+ """
+ h = super().forward(x)
+ h = h.type(x.dtype)
+ h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
+
+ if self.mode == "classification":
+ h_class = self.classification_layer(h)
+ return self.to_material_classification(h_class)
+ else: # default to regression mode
+ h_reg = self.out_layer(h)
+ return self.to_material_properties(h_reg)
+
+
+class ElasticSLatMaterialDecoder(SparseTransformerElasticMixin, SLatMaterialDecoder):
+ """
+ SLat material decoder with elastic memory management.
+ Used for training with low VRAM.
+ """
+
+ pass
diff --git a/deps/vomp/vomp/models/structured_latent_vae/encoder.py b/deps/vomp/vomp/models/structured_latent_vae/encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..a96013dbc0ce0e19f056cae2d7b9b1c63693da21
--- /dev/null
+++ b/deps/vomp/vomp/models/structured_latent_vae/encoder.py
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from ...modules import sparse as sp
+from .base import SparseTransformerBase
+from ..sparse_elastic_mixin import SparseTransformerElasticMixin
+
+
+class SLatEncoder(SparseTransformerBase):
+ def __init__(
+ self,
+ resolution: int,
+ in_channels: int,
+ model_channels: int,
+ latent_channels: int,
+ num_blocks: int,
+ num_heads: Optional[int] = None,
+ num_head_channels: Optional[int] = 64,
+ mlp_ratio: float = 4,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "swin",
+ window_size: int = 8,
+ pe_mode: Literal["ape", "rope"] = "ape",
+ use_fp16: bool = False,
+ use_checkpoint: bool = False,
+ qk_rms_norm: bool = False,
+ ):
+ print("Creating SLatEncoder with:")
+ print(f" - Resolution: {resolution}")
+ print(f" - Input channels: {in_channels}")
+ print(f" - Model channels: {model_channels}")
+ print(f" - Latent channels: {latent_channels}")
+ print(f" - Num blocks: {num_blocks}")
+ print(f" - Attention mode: {attn_mode}")
+
+ super().__init__(
+ in_channels=in_channels,
+ model_channels=model_channels,
+ num_blocks=num_blocks,
+ num_heads=num_heads,
+ num_head_channels=num_head_channels,
+ mlp_ratio=mlp_ratio,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ pe_mode=pe_mode,
+ use_fp16=use_fp16,
+ use_checkpoint=use_checkpoint,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.resolution = resolution
+ self.out_layer = sp.SparseLinear(model_channels, 2 * latent_channels)
+
+ self.initialize_weights()
+ if use_fp16:
+ self.convert_to_fp16()
+ print(" - Using FP16 precision")
+ print("Created new encoder for training.")
+
+ def initialize_weights(self) -> None:
+ super().initialize_weights()
+ # Zero-out output layers:
+ nn.init.constant_(self.out_layer.weight, 0)
+ nn.init.constant_(self.out_layer.bias, 0)
+
+ def forward(self, x: sp.SparseTensor, sample_posterior=True, return_raw=False):
+ h = super().forward(x)
+ h = h.type(x.dtype)
+ h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
+ h = self.out_layer(h)
+
+ # Sample from the posterior distribution
+ mean, logvar = h.feats.chunk(2, dim=-1)
+ if sample_posterior:
+ std = torch.exp(0.5 * logvar)
+ z = mean + std * torch.randn_like(std)
+ else:
+ z = mean
+ z = h.replace(z)
+
+ if return_raw:
+ return z, mean, logvar
+ else:
+ return z
+
+
+class ElasticSLatEncoder(SparseTransformerElasticMixin, SLatEncoder):
+ """
+ SLat VAE encoder with elastic memory management.
+ Used for training with low VRAM.
+ """
diff --git a/deps/vomp/vomp/modules/__init__.py b/deps/vomp/vomp/modules/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/modules/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/modules/attention/__init__.py b/deps/vomp/vomp/modules/attention/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..0645c04b19ccc17f2f836d050df5eb30435a8b84
--- /dev/null
+++ b/deps/vomp/vomp/modules/attention/__init__.py
@@ -0,0 +1,58 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+
+BACKEND = "flash_attn"
+DEBUG = False
+
+
+def __from_env():
+ import os
+
+ global BACKEND
+ global DEBUG
+
+ env_attn_backend = os.environ.get("ATTN_BACKEND")
+ env_sttn_debug = os.environ.get("ATTN_DEBUG")
+
+ if env_attn_backend is not None and env_attn_backend in [
+ "xformers",
+ "flash_attn",
+ "sdpa",
+ "naive",
+ ]:
+ BACKEND = env_attn_backend
+ if env_sttn_debug is not None:
+ DEBUG = env_sttn_debug == "1"
+
+ print(f"[ATTENTION] Using backend: {BACKEND}")
+
+
+__from_env()
+
+
+def set_backend(backend: Literal["xformers", "flash_attn"]):
+ global BACKEND
+ BACKEND = backend
+
+
+def set_debug(debug: bool):
+ global DEBUG
+ DEBUG = debug
+
+
+from .full_attn import *
+from .modules import *
diff --git a/deps/vomp/vomp/modules/attention/full_attn.py b/deps/vomp/vomp/modules/attention/full_attn.py
new file mode 100755
index 0000000000000000000000000000000000000000..9c0243be3b4fd83a776ae0fa547b62aa0c0da067
--- /dev/null
+++ b/deps/vomp/vomp/modules/attention/full_attn.py
@@ -0,0 +1,174 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import math
+from . import DEBUG, BACKEND
+
+if BACKEND == "xformers":
+ import xformers.ops as xops
+elif BACKEND == "flash_attn":
+ import flash_attn
+elif BACKEND == "sdpa":
+ from torch.nn.functional import scaled_dot_product_attention as sdpa
+elif BACKEND == "naive":
+ pass
+else:
+ raise ValueError(f"Unknown attention backend: {BACKEND}")
+
+
+__all__ = [
+ "scaled_dot_product_attention",
+]
+
+
+def _naive_sdpa(q, k, v):
+ """
+ Naive implementation of scaled dot product attention.
+ """
+ q = q.permute(0, 2, 1, 3) # [N, H, L, C]
+ k = k.permute(0, 2, 1, 3) # [N, H, L, C]
+ v = v.permute(0, 2, 1, 3) # [N, H, L, C]
+ scale_factor = 1 / math.sqrt(q.size(-1))
+ attn_weight = q @ k.transpose(-2, -1) * scale_factor
+ attn_weight = torch.softmax(attn_weight, dim=-1)
+ out = attn_weight @ v
+ out = out.permute(0, 2, 1, 3) # [N, L, H, C]
+ return out
+
+
+@overload
+def scaled_dot_product_attention(qkv: torch.Tensor) -> torch.Tensor:
+ """
+ Apply scaled dot product attention.
+
+ Args:
+ qkv (torch.Tensor): A [N, L, 3, H, C] tensor containing Qs, Ks, and Vs.
+ """
+ ...
+
+
+@overload
+def scaled_dot_product_attention(q: torch.Tensor, kv: torch.Tensor) -> torch.Tensor:
+ """
+ Apply scaled dot product attention.
+
+ Args:
+ q (torch.Tensor): A [N, L, H, C] tensor containing Qs.
+ kv (torch.Tensor): A [N, L, 2, H, C] tensor containing Ks and Vs.
+ """
+ ...
+
+
+@overload
+def scaled_dot_product_attention(
+ q: torch.Tensor, k: torch.Tensor, v: torch.Tensor
+) -> torch.Tensor:
+ """
+ Apply scaled dot product attention.
+
+ Args:
+ q (torch.Tensor): A [N, L, H, Ci] tensor containing Qs.
+ k (torch.Tensor): A [N, L, H, Ci] tensor containing Ks.
+ v (torch.Tensor): A [N, L, H, Co] tensor containing Vs.
+
+ Note:
+ k and v are assumed to have the same coordinate map.
+ """
+ ...
+
+
+def scaled_dot_product_attention(*args, **kwargs):
+ arg_names_dict = {1: ["qkv"], 2: ["q", "kv"], 3: ["q", "k", "v"]}
+ num_all_args = len(args) + len(kwargs)
+ assert (
+ num_all_args in arg_names_dict
+ ), f"Invalid number of arguments, got {num_all_args}, expected 1, 2, or 3"
+ for key in arg_names_dict[num_all_args][len(args) :]:
+ assert key in kwargs, f"Missing argument {key}"
+
+ if num_all_args == 1:
+ qkv = args[0] if len(args) > 0 else kwargs["qkv"]
+ assert (
+ len(qkv.shape) == 5 and qkv.shape[2] == 3
+ ), f"Invalid shape for qkv, got {qkv.shape}, expected [N, L, 3, H, C]"
+ device = qkv.device
+
+ elif num_all_args == 2:
+ q = args[0] if len(args) > 0 else kwargs["q"]
+ kv = args[1] if len(args) > 1 else kwargs["kv"]
+ assert (
+ q.shape[0] == kv.shape[0]
+ ), f"Batch size mismatch, got {q.shape[0]} and {kv.shape[0]}"
+ assert (
+ len(q.shape) == 4
+ ), f"Invalid shape for q, got {q.shape}, expected [N, L, H, C]"
+ assert (
+ len(kv.shape) == 5
+ ), f"Invalid shape for kv, got {kv.shape}, expected [N, L, 2, H, C]"
+ device = q.device
+
+ elif num_all_args == 3:
+ q = args[0] if len(args) > 0 else kwargs["q"]
+ k = args[1] if len(args) > 1 else kwargs["k"]
+ v = args[2] if len(args) > 2 else kwargs["v"]
+ assert (
+ q.shape[0] == k.shape[0] == v.shape[0]
+ ), f"Batch size mismatch, got {q.shape[0]}, {k.shape[0]}, and {v.shape[0]}"
+ assert (
+ len(q.shape) == 4
+ ), f"Invalid shape for q, got {q.shape}, expected [N, L, H, Ci]"
+ assert (
+ len(k.shape) == 4
+ ), f"Invalid shape for k, got {k.shape}, expected [N, L, H, Ci]"
+ assert (
+ len(v.shape) == 4
+ ), f"Invalid shape for v, got {v.shape}, expected [N, L, H, Co]"
+ device = q.device
+
+ if BACKEND == "xformers":
+ if num_all_args == 1:
+ q, k, v = qkv.unbind(dim=2)
+ elif num_all_args == 2:
+ k, v = kv.unbind(dim=2)
+ out = xops.memory_efficient_attention(q, k, v)
+ elif BACKEND == "flash_attn":
+ if num_all_args == 1:
+ out = flash_attn.flash_attn_qkvpacked_func(qkv)
+ elif num_all_args == 2:
+ out = flash_attn.flash_attn_kvpacked_func(q, kv)
+ elif num_all_args == 3:
+ out = flash_attn.flash_attn_func(q, k, v)
+ elif BACKEND == "sdpa":
+ if num_all_args == 1:
+ q, k, v = qkv.unbind(dim=2)
+ elif num_all_args == 2:
+ k, v = kv.unbind(dim=2)
+ q = q.permute(0, 2, 1, 3) # [N, H, L, C]
+ k = k.permute(0, 2, 1, 3) # [N, H, L, C]
+ v = v.permute(0, 2, 1, 3) # [N, H, L, C]
+ out = sdpa(q, k, v) # [N, H, L, C]
+ out = out.permute(0, 2, 1, 3) # [N, L, H, C]
+ elif BACKEND == "naive":
+ if num_all_args == 1:
+ q, k, v = qkv.unbind(dim=2)
+ elif num_all_args == 2:
+ k, v = kv.unbind(dim=2)
+ out = _naive_sdpa(q, k, v)
+ else:
+ raise ValueError(f"Unknown attention module: {BACKEND}")
+
+ return out
diff --git a/deps/vomp/vomp/modules/attention/modules.py b/deps/vomp/vomp/modules/attention/modules.py
new file mode 100755
index 0000000000000000000000000000000000000000..e9006b1ffdac290fbaf60fb1a10c633ae4a7f13b
--- /dev/null
+++ b/deps/vomp/vomp/modules/attention/modules.py
@@ -0,0 +1,186 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from .full_attn import scaled_dot_product_attention
+
+
+class MultiHeadRMSNorm(nn.Module):
+ def __init__(self, dim: int, heads: int):
+ super().__init__()
+ self.scale = dim**0.5
+ self.gamma = nn.Parameter(torch.ones(heads, dim))
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return (F.normalize(x.float(), dim=-1) * self.gamma * self.scale).to(x.dtype)
+
+
+class RotaryPositionEmbedder(nn.Module):
+ def __init__(self, hidden_size: int, in_channels: int = 3):
+ super().__init__()
+ assert hidden_size % 2 == 0, "Hidden size must be divisible by 2"
+ self.hidden_size = hidden_size
+ self.in_channels = in_channels
+ self.freq_dim = hidden_size // in_channels // 2
+ self.freqs = torch.arange(self.freq_dim, dtype=torch.float32) / self.freq_dim
+ self.freqs = 1.0 / (10000**self.freqs)
+
+ def _get_phases(self, indices: torch.Tensor) -> torch.Tensor:
+ self.freqs = self.freqs.to(indices.device)
+ phases = torch.outer(indices, self.freqs)
+ phases = torch.polar(torch.ones_like(phases), phases)
+ return phases
+
+ def _rotary_embedding(self, x: torch.Tensor, phases: torch.Tensor) -> torch.Tensor:
+ x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
+ x_rotated = x_complex * phases
+ x_embed = (
+ torch.view_as_real(x_rotated).reshape(*x_rotated.shape[:-1], -1).to(x.dtype)
+ )
+ return x_embed
+
+ def forward(
+ self, q: torch.Tensor, k: torch.Tensor, indices: Optional[torch.Tensor] = None
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Args:
+ q (sp.SparseTensor): [..., N, D] tensor of queries
+ k (sp.SparseTensor): [..., N, D] tensor of keys
+ indices (torch.Tensor): [..., N, C] tensor of spatial positions
+ """
+ if indices is None:
+ indices = torch.arange(q.shape[-2], device=q.device)
+ if len(q.shape) > 2:
+ indices = indices.unsqueeze(0).expand(q.shape[:-2] + (-1,))
+
+ phases = self._get_phases(indices.reshape(-1)).reshape(*indices.shape[:-1], -1)
+ if phases.shape[1] < self.hidden_size // 2:
+ phases = torch.cat(
+ [
+ phases,
+ torch.polar(
+ torch.ones(
+ *phases.shape[:-1],
+ self.hidden_size // 2 - phases.shape[1],
+ device=phases.device,
+ ),
+ torch.zeros(
+ *phases.shape[:-1],
+ self.hidden_size // 2 - phases.shape[1],
+ device=phases.device,
+ ),
+ ),
+ ],
+ dim=-1,
+ )
+ q_embed = self._rotary_embedding(q, phases)
+ k_embed = self._rotary_embedding(k, phases)
+ return q_embed, k_embed
+
+
+class MultiHeadAttention(nn.Module):
+ def __init__(
+ self,
+ channels: int,
+ num_heads: int,
+ ctx_channels: Optional[int] = None,
+ type: Literal["self", "cross"] = "self",
+ attn_mode: Literal["full", "windowed"] = "full",
+ window_size: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ qkv_bias: bool = True,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ ):
+ super().__init__()
+ assert channels % num_heads == 0
+ assert type in ["self", "cross"], f"Invalid attention type: {type}"
+ assert attn_mode in ["full", "windowed"], f"Invalid attention mode: {attn_mode}"
+ assert (
+ type == "self" or attn_mode == "full"
+ ), "Cross-attention only supports full attention"
+
+ if attn_mode == "windowed":
+ raise NotImplementedError("Windowed attention is not yet implemented")
+
+ self.channels = channels
+ self.head_dim = channels // num_heads
+ self.ctx_channels = ctx_channels if ctx_channels is not None else channels
+ self.num_heads = num_heads
+ self._type = type
+ self.attn_mode = attn_mode
+ self.window_size = window_size
+ self.shift_window = shift_window
+ self.use_rope = use_rope
+ self.qk_rms_norm = qk_rms_norm
+
+ if self._type == "self":
+ self.to_qkv = nn.Linear(channels, channels * 3, bias=qkv_bias)
+ else:
+ self.to_q = nn.Linear(channels, channels, bias=qkv_bias)
+ self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=qkv_bias)
+
+ if self.qk_rms_norm:
+ self.q_rms_norm = MultiHeadRMSNorm(self.head_dim, num_heads)
+ self.k_rms_norm = MultiHeadRMSNorm(self.head_dim, num_heads)
+
+ self.to_out = nn.Linear(channels, channels)
+
+ if use_rope:
+ self.rope = RotaryPositionEmbedder(channels)
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ context: Optional[torch.Tensor] = None,
+ indices: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ B, L, C = x.shape
+ if self._type == "self":
+ qkv = self.to_qkv(x)
+ qkv = qkv.reshape(B, L, 3, self.num_heads, -1)
+ if self.use_rope:
+ q, k, v = qkv.unbind(dim=2)
+ q, k = self.rope(q, k, indices)
+ qkv = torch.stack([q, k, v], dim=2)
+ if self.attn_mode == "full":
+ if self.qk_rms_norm:
+ q, k, v = qkv.unbind(dim=2)
+ q = self.q_rms_norm(q)
+ k = self.k_rms_norm(k)
+ h = scaled_dot_product_attention(q, k, v)
+ else:
+ h = scaled_dot_product_attention(qkv)
+ elif self.attn_mode == "windowed":
+ raise NotImplementedError("Windowed attention is not yet implemented")
+ else:
+ Lkv = context.shape[1]
+ q = self.to_q(x)
+ kv = self.to_kv(context)
+ q = q.reshape(B, L, self.num_heads, -1)
+ kv = kv.reshape(B, Lkv, 2, self.num_heads, -1)
+ if self.qk_rms_norm:
+ q = self.q_rms_norm(q)
+ k, v = kv.unbind(dim=2)
+ k = self.k_rms_norm(k)
+ h = scaled_dot_product_attention(q, k, v)
+ else:
+ h = scaled_dot_product_attention(q, kv)
+ h = h.reshape(B, L, -1)
+ h = self.to_out(h)
+ return h
diff --git a/deps/vomp/vomp/modules/norm.py b/deps/vomp/vomp/modules/norm.py
new file mode 100644
index 0000000000000000000000000000000000000000..758dedee82b2fbfd19833b4c4d70a775713c35f2
--- /dev/null
+++ b/deps/vomp/vomp/modules/norm.py
@@ -0,0 +1,40 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn as nn
+
+
+class LayerNorm32(nn.LayerNorm):
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return super().forward(x.float()).type(x.dtype)
+
+
+class GroupNorm32(nn.GroupNorm):
+ """
+ A GroupNorm layer that converts to float32 before the forward pass.
+ """
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return super().forward(x.float()).type(x.dtype)
+
+
+class ChannelLayerNorm32(LayerNorm32):
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ DIM = x.dim()
+ x = x.permute(0, *range(2, DIM), 1).contiguous()
+ x = super().forward(x)
+ x = x.permute(0, DIM - 1, *range(1, DIM - 1)).contiguous()
+ return x
diff --git a/deps/vomp/vomp/modules/sparse/__init__.py b/deps/vomp/vomp/modules/sparse/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..bae280254084b0be0d719b435df47dd047ae6092
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/__init__.py
@@ -0,0 +1,124 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+
+BACKEND = "spconv"
+DEBUG = False
+ATTN = "flash_attn"
+
+
+def __from_env():
+ import os
+
+ global BACKEND
+ global DEBUG
+ global ATTN
+
+ env_sparse_backend = os.environ.get("SPARSE_BACKEND")
+ env_sparse_debug = os.environ.get("SPARSE_DEBUG")
+ env_sparse_attn = os.environ.get("SPARSE_ATTN_BACKEND")
+ if env_sparse_attn is None:
+ env_sparse_attn = os.environ.get("ATTN_BACKEND")
+
+ if env_sparse_backend is not None and env_sparse_backend in [
+ "spconv",
+ "torchsparse",
+ ]:
+ BACKEND = env_sparse_backend
+ if env_sparse_debug is not None:
+ DEBUG = env_sparse_debug == "1"
+ if env_sparse_attn is not None and env_sparse_attn in ["xformers", "flash_attn"]:
+ ATTN = env_sparse_attn
+
+ print(f"[SPARSE] Backend: {BACKEND}, Attention: {ATTN}")
+
+
+__from_env()
+
+
+def set_backend(backend: Literal["spconv", "torchsparse"]):
+ global BACKEND
+ BACKEND = backend
+
+
+def set_debug(debug: bool):
+ global DEBUG
+ DEBUG = debug
+
+
+def set_attn(attn: Literal["xformers", "flash_attn"]):
+ global ATTN
+ ATTN = attn
+
+
+import importlib
+
+__attributes = {
+ "SparseTensor": "basic",
+ "sparse_batch_broadcast": "basic",
+ "sparse_batch_op": "basic",
+ "sparse_cat": "basic",
+ "sparse_unbind": "basic",
+ "SparseGroupNorm": "norm",
+ "SparseLayerNorm": "norm",
+ "SparseGroupNorm32": "norm",
+ "SparseLayerNorm32": "norm",
+ "SparseReLU": "nonlinearity",
+ "SparseSiLU": "nonlinearity",
+ "SparseGELU": "nonlinearity",
+ "SparseActivation": "nonlinearity",
+ "SparseLinear": "linear",
+ "sparse_scaled_dot_product_attention": "attention",
+ "SerializeMode": "attention",
+ "sparse_serialized_scaled_dot_product_self_attention": "attention",
+ "sparse_windowed_scaled_dot_product_self_attention": "attention",
+ "SparseMultiHeadAttention": "attention",
+ "SparseConv3d": "conv",
+ "SparseInverseConv3d": "conv",
+ "SparseDownsample": "spatial",
+ "SparseUpsample": "spatial",
+ "SparseSubdivide": "spatial",
+}
+
+__submodules = ["transformer"]
+
+__all__ = list(__attributes.keys()) + __submodules
+
+
+def __getattr__(name):
+ if name not in globals():
+ if name in __attributes:
+ module_name = __attributes[name]
+ module = importlib.import_module(f".{module_name}", __name__)
+ globals()[name] = getattr(module, name)
+ elif name in __submodules:
+ module = importlib.import_module(f".{name}", __name__)
+ globals()[name] = module
+ else:
+ raise AttributeError(f"module {__name__} has no attribute {name}")
+ return globals()[name]
+
+
+# For Pylance
+if __name__ == "__main__":
+ from .basic import *
+ from .norm import *
+ from .nonlinearity import *
+ from .linear import *
+ from .attention import *
+ from .conv import *
+ from .spatial import *
+ import transformer
diff --git a/deps/vomp/vomp/modules/sparse/attention/__init__.py b/deps/vomp/vomp/modules/sparse/attention/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..2307ee4067b777f05045ad5ca825f733a647ddac
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/attention/__init__.py
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .full_attn import *
+from .serialized_attn import *
+from .windowed_attn import *
+from .modules import *
diff --git a/deps/vomp/vomp/modules/sparse/attention/full_attn.py b/deps/vomp/vomp/modules/sparse/attention/full_attn.py
new file mode 100755
index 0000000000000000000000000000000000000000..46c5673babd298128c66c59837445468f1e4e181
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/attention/full_attn.py
@@ -0,0 +1,313 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+from .. import SparseTensor
+from .. import DEBUG, ATTN
+
+if ATTN == "xformers":
+ import xformers.ops as xops
+elif ATTN == "flash_attn":
+ import flash_attn
+else:
+ raise ValueError(f"Unknown attention module: {ATTN}")
+
+
+__all__ = [
+ "sparse_scaled_dot_product_attention",
+]
+
+
+@overload
+def sparse_scaled_dot_product_attention(qkv: SparseTensor) -> SparseTensor:
+ """
+ Apply scaled dot product attention to a sparse tensor.
+
+ Args:
+ qkv (SparseTensor): A [N, *, 3, H, C] sparse tensor containing Qs, Ks, and Vs.
+ """
+ ...
+
+
+@overload
+def sparse_scaled_dot_product_attention(
+ q: SparseTensor, kv: Union[SparseTensor, torch.Tensor]
+) -> SparseTensor:
+ """
+ Apply scaled dot product attention to a sparse tensor.
+
+ Args:
+ q (SparseTensor): A [N, *, H, C] sparse tensor containing Qs.
+ kv (SparseTensor or torch.Tensor): A [N, *, 2, H, C] sparse tensor or a [N, L, 2, H, C] dense tensor containing Ks and Vs.
+ """
+ ...
+
+
+@overload
+def sparse_scaled_dot_product_attention(
+ q: torch.Tensor, kv: SparseTensor
+) -> torch.Tensor:
+ """
+ Apply scaled dot product attention to a sparse tensor.
+
+ Args:
+ q (SparseTensor): A [N, L, H, C] dense tensor containing Qs.
+ kv (SparseTensor or torch.Tensor): A [N, *, 2, H, C] sparse tensor containing Ks and Vs.
+ """
+ ...
+
+
+@overload
+def sparse_scaled_dot_product_attention(
+ q: SparseTensor, k: SparseTensor, v: SparseTensor
+) -> SparseTensor:
+ """
+ Apply scaled dot product attention to a sparse tensor.
+
+ Args:
+ q (SparseTensor): A [N, *, H, Ci] sparse tensor containing Qs.
+ k (SparseTensor): A [N, *, H, Ci] sparse tensor containing Ks.
+ v (SparseTensor): A [N, *, H, Co] sparse tensor containing Vs.
+
+ Note:
+ k and v are assumed to have the same coordinate map.
+ """
+ ...
+
+
+@overload
+def sparse_scaled_dot_product_attention(
+ q: SparseTensor, k: torch.Tensor, v: torch.Tensor
+) -> SparseTensor:
+ """
+ Apply scaled dot product attention to a sparse tensor.
+
+ Args:
+ q (SparseTensor): A [N, *, H, Ci] sparse tensor containing Qs.
+ k (torch.Tensor): A [N, L, H, Ci] dense tensor containing Ks.
+ v (torch.Tensor): A [N, L, H, Co] dense tensor containing Vs.
+ """
+ ...
+
+
+@overload
+def sparse_scaled_dot_product_attention(
+ q: torch.Tensor, k: SparseTensor, v: SparseTensor
+) -> torch.Tensor:
+ """
+ Apply scaled dot product attention to a sparse tensor.
+
+ Args:
+ q (torch.Tensor): A [N, L, H, Ci] dense tensor containing Qs.
+ k (SparseTensor): A [N, *, H, Ci] sparse tensor containing Ks.
+ v (SparseTensor): A [N, *, H, Co] sparse tensor containing Vs.
+ """
+ ...
+
+
+def sparse_scaled_dot_product_attention(*args, **kwargs):
+ arg_names_dict = {1: ["qkv"], 2: ["q", "kv"], 3: ["q", "k", "v"]}
+ num_all_args = len(args) + len(kwargs)
+ assert (
+ num_all_args in arg_names_dict
+ ), f"Invalid number of arguments, got {num_all_args}, expected 1, 2, or 3"
+ for key in arg_names_dict[num_all_args][len(args) :]:
+ assert key in kwargs, f"Missing argument {key}"
+
+ if num_all_args == 1:
+ qkv = args[0] if len(args) > 0 else kwargs["qkv"]
+ assert isinstance(
+ qkv, SparseTensor
+ ), f"qkv must be a SparseTensor, got {type(qkv)}"
+ assert (
+ len(qkv.shape) == 4 and qkv.shape[1] == 3
+ ), f"Invalid shape for qkv, got {qkv.shape}, expected [N, *, 3, H, C]"
+ device = qkv.device
+
+ s = qkv
+ q_seqlen = [
+ qkv.layout[i].stop - qkv.layout[i].start for i in range(qkv.shape[0])
+ ]
+ kv_seqlen = q_seqlen
+ qkv = qkv.feats # [T, 3, H, C]
+
+ elif num_all_args == 2:
+ q = args[0] if len(args) > 0 else kwargs["q"]
+ kv = args[1] if len(args) > 1 else kwargs["kv"]
+ assert (
+ isinstance(q, SparseTensor)
+ and isinstance(kv, (SparseTensor, torch.Tensor))
+ or isinstance(q, torch.Tensor)
+ and isinstance(kv, SparseTensor)
+ ), f"Invalid types, got {type(q)} and {type(kv)}"
+ assert (
+ q.shape[0] == kv.shape[0]
+ ), f"Batch size mismatch, got {q.shape[0]} and {kv.shape[0]}"
+ device = q.device
+
+ if isinstance(q, SparseTensor):
+ assert (
+ len(q.shape) == 3
+ ), f"Invalid shape for q, got {q.shape}, expected [N, *, H, C]"
+ s = q
+ q_seqlen = [q.layout[i].stop - q.layout[i].start for i in range(q.shape[0])]
+ q = q.feats # [T_Q, H, C]
+ else:
+ assert (
+ len(q.shape) == 4
+ ), f"Invalid shape for q, got {q.shape}, expected [N, L, H, C]"
+ s = None
+ N, L, H, C = q.shape
+ q_seqlen = [L] * N
+ q = q.reshape(N * L, H, C) # [T_Q, H, C]
+
+ if isinstance(kv, SparseTensor):
+ assert (
+ len(kv.shape) == 4 and kv.shape[1] == 2
+ ), f"Invalid shape for kv, got {kv.shape}, expected [N, *, 2, H, C]"
+ kv_seqlen = [
+ kv.layout[i].stop - kv.layout[i].start for i in range(kv.shape[0])
+ ]
+ kv = kv.feats # [T_KV, 2, H, C]
+ else:
+ assert (
+ len(kv.shape) == 5
+ ), f"Invalid shape for kv, got {kv.shape}, expected [N, L, 2, H, C]"
+ N, L, _, H, C = kv.shape
+ kv_seqlen = [L] * N
+ kv = kv.reshape(N * L, 2, H, C) # [T_KV, 2, H, C]
+
+ elif num_all_args == 3:
+ q = args[0] if len(args) > 0 else kwargs["q"]
+ k = args[1] if len(args) > 1 else kwargs["k"]
+ v = args[2] if len(args) > 2 else kwargs["v"]
+ assert (
+ isinstance(q, SparseTensor)
+ and isinstance(k, (SparseTensor, torch.Tensor))
+ and type(k) == type(v)
+ or isinstance(q, torch.Tensor)
+ and isinstance(k, SparseTensor)
+ and isinstance(v, SparseTensor)
+ ), f"Invalid types, got {type(q)}, {type(k)}, and {type(v)}"
+ assert (
+ q.shape[0] == k.shape[0] == v.shape[0]
+ ), f"Batch size mismatch, got {q.shape[0]}, {k.shape[0]}, and {v.shape[0]}"
+ device = q.device
+
+ if isinstance(q, SparseTensor):
+ assert (
+ len(q.shape) == 3
+ ), f"Invalid shape for q, got {q.shape}, expected [N, *, H, Ci]"
+ s = q
+ q_seqlen = [q.layout[i].stop - q.layout[i].start for i in range(q.shape[0])]
+ q = q.feats # [T_Q, H, Ci]
+ else:
+ assert (
+ len(q.shape) == 4
+ ), f"Invalid shape for q, got {q.shape}, expected [N, L, H, Ci]"
+ s = None
+ N, L, H, CI = q.shape
+ q_seqlen = [L] * N
+ q = q.reshape(N * L, H, CI) # [T_Q, H, Ci]
+
+ if isinstance(k, SparseTensor):
+ assert (
+ len(k.shape) == 3
+ ), f"Invalid shape for k, got {k.shape}, expected [N, *, H, Ci]"
+ assert (
+ len(v.shape) == 3
+ ), f"Invalid shape for v, got {v.shape}, expected [N, *, H, Co]"
+ kv_seqlen = [
+ k.layout[i].stop - k.layout[i].start for i in range(k.shape[0])
+ ]
+ k = k.feats # [T_KV, H, Ci]
+ v = v.feats # [T_KV, H, Co]
+ else:
+ assert (
+ len(k.shape) == 4
+ ), f"Invalid shape for k, got {k.shape}, expected [N, L, H, Ci]"
+ assert (
+ len(v.shape) == 4
+ ), f"Invalid shape for v, got {v.shape}, expected [N, L, H, Co]"
+ N, L, H, CI, CO = *k.shape, v.shape[-1]
+ kv_seqlen = [L] * N
+ k = k.reshape(N * L, H, CI) # [T_KV, H, Ci]
+ v = v.reshape(N * L, H, CO) # [T_KV, H, Co]
+
+ if DEBUG:
+ if s is not None:
+ for i in range(s.shape[0]):
+ assert (
+ s.coords[s.layout[i]] == i
+ ).all(), f"SparseScaledDotProductSelfAttention: batch index mismatch"
+ if num_all_args in [2, 3]:
+ assert q.shape[:2] == [
+ 1,
+ sum(q_seqlen),
+ ], f"SparseScaledDotProductSelfAttention: q shape mismatch"
+ if num_all_args == 3:
+ assert k.shape[:2] == [
+ 1,
+ sum(kv_seqlen),
+ ], f"SparseScaledDotProductSelfAttention: k shape mismatch"
+ assert v.shape[:2] == [
+ 1,
+ sum(kv_seqlen),
+ ], f"SparseScaledDotProductSelfAttention: v shape mismatch"
+
+ if ATTN == "xformers":
+ if num_all_args == 1:
+ q, k, v = qkv.unbind(dim=1)
+ elif num_all_args == 2:
+ k, v = kv.unbind(dim=1)
+ q = q.unsqueeze(0)
+ k = k.unsqueeze(0)
+ v = v.unsqueeze(0)
+ mask = xops.fmha.BlockDiagonalMask.from_seqlens(q_seqlen, kv_seqlen)
+ out = xops.memory_efficient_attention(q, k, v, mask)[0]
+ elif ATTN == "flash_attn":
+ cu_seqlens_q = (
+ torch.cat([torch.tensor([0]), torch.cumsum(torch.tensor(q_seqlen), dim=0)])
+ .int()
+ .to(device)
+ )
+ if num_all_args in [2, 3]:
+ cu_seqlens_kv = (
+ torch.cat(
+ [torch.tensor([0]), torch.cumsum(torch.tensor(kv_seqlen), dim=0)]
+ )
+ .int()
+ .to(device)
+ )
+ if num_all_args == 1:
+ out = flash_attn.flash_attn_varlen_qkvpacked_func(
+ qkv, cu_seqlens_q, max(q_seqlen)
+ )
+ elif num_all_args == 2:
+ out = flash_attn.flash_attn_varlen_kvpacked_func(
+ q, kv, cu_seqlens_q, cu_seqlens_kv, max(q_seqlen), max(kv_seqlen)
+ )
+ elif num_all_args == 3:
+ out = flash_attn.flash_attn_varlen_func(
+ q, k, v, cu_seqlens_q, cu_seqlens_kv, max(q_seqlen), max(kv_seqlen)
+ )
+ else:
+ raise ValueError(f"Unknown attention module: {ATTN}")
+
+ if s is not None:
+ return s.replace(out)
+ else:
+ return out.reshape(N, L, H, -1)
diff --git a/deps/vomp/vomp/modules/sparse/attention/modules.py b/deps/vomp/vomp/modules/sparse/attention/modules.py
new file mode 100755
index 0000000000000000000000000000000000000000..2a5b0bfa2a4b0ff1a6e31236bef91b86e797692f
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/attention/modules.py
@@ -0,0 +1,181 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from .. import SparseTensor
+from .full_attn import sparse_scaled_dot_product_attention
+from .serialized_attn import (
+ SerializeMode,
+ sparse_serialized_scaled_dot_product_self_attention,
+)
+from .windowed_attn import sparse_windowed_scaled_dot_product_self_attention
+from ...attention import RotaryPositionEmbedder
+
+
+class SparseMultiHeadRMSNorm(nn.Module):
+ def __init__(self, dim: int, heads: int):
+ super().__init__()
+ self.scale = dim**0.5
+ self.gamma = nn.Parameter(torch.ones(heads, dim))
+
+ def forward(
+ self, x: Union[SparseTensor, torch.Tensor]
+ ) -> Union[SparseTensor, torch.Tensor]:
+ x_type = x.dtype
+ x = x.float()
+ if isinstance(x, SparseTensor):
+ x = x.replace(F.normalize(x.feats, dim=-1))
+ else:
+ x = F.normalize(x, dim=-1)
+ return (x * self.gamma * self.scale).to(x_type)
+
+
+class SparseMultiHeadAttention(nn.Module):
+ def __init__(
+ self,
+ channels: int,
+ num_heads: int,
+ ctx_channels: Optional[int] = None,
+ type: Literal["self", "cross"] = "self",
+ attn_mode: Literal["full", "serialized", "windowed"] = "full",
+ window_size: Optional[int] = None,
+ shift_sequence: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ serialize_mode: Optional[SerializeMode] = None,
+ qkv_bias: bool = True,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ ):
+ super().__init__()
+ assert channels % num_heads == 0
+ assert type in ["self", "cross"], f"Invalid attention type: {type}"
+ assert attn_mode in [
+ "full",
+ "serialized",
+ "windowed",
+ ], f"Invalid attention mode: {attn_mode}"
+ assert (
+ type == "self" or attn_mode == "full"
+ ), "Cross-attention only supports full attention"
+ assert (
+ type == "self" or use_rope is False
+ ), "Rotary position embeddings only supported for self-attention"
+ self.channels = channels
+ self.ctx_channels = ctx_channels if ctx_channels is not None else channels
+ self.num_heads = num_heads
+ self._type = type
+ self.attn_mode = attn_mode
+ self.window_size = window_size
+ self.shift_sequence = shift_sequence
+ self.shift_window = shift_window
+ self.serialize_mode = serialize_mode
+ self.use_rope = use_rope
+ self.qk_rms_norm = qk_rms_norm
+
+ if self._type == "self":
+ self.to_qkv = nn.Linear(channels, channels * 3, bias=qkv_bias)
+ else:
+ self.to_q = nn.Linear(channels, channels, bias=qkv_bias)
+ self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=qkv_bias)
+
+ if self.qk_rms_norm:
+ self.q_rms_norm = SparseMultiHeadRMSNorm(channels // num_heads, num_heads)
+ self.k_rms_norm = SparseMultiHeadRMSNorm(channels // num_heads, num_heads)
+
+ self.to_out = nn.Linear(channels, channels)
+
+ if use_rope:
+ self.rope = RotaryPositionEmbedder(channels)
+
+ @staticmethod
+ def _linear(
+ module: nn.Linear, x: Union[SparseTensor, torch.Tensor]
+ ) -> Union[SparseTensor, torch.Tensor]:
+ if isinstance(x, SparseTensor):
+ return x.replace(module(x.feats))
+ else:
+ return module(x)
+
+ @staticmethod
+ def _reshape_chs(
+ x: Union[SparseTensor, torch.Tensor], shape: Tuple[int, ...]
+ ) -> Union[SparseTensor, torch.Tensor]:
+ if isinstance(x, SparseTensor):
+ return x.reshape(*shape)
+ else:
+ return x.reshape(*x.shape[:2], *shape)
+
+ def _fused_pre(
+ self, x: Union[SparseTensor, torch.Tensor], num_fused: int
+ ) -> Union[SparseTensor, torch.Tensor]:
+ if isinstance(x, SparseTensor):
+ x_feats = x.feats.unsqueeze(0)
+ else:
+ x_feats = x
+ x_feats = x_feats.reshape(*x_feats.shape[:2], num_fused, self.num_heads, -1)
+ return x.replace(x_feats.squeeze(0)) if isinstance(x, SparseTensor) else x_feats
+
+ def _rope(self, qkv: SparseTensor) -> SparseTensor:
+ q, k, v = qkv.feats.unbind(dim=1) # [T, H, C]
+ q, k = self.rope(q, k, qkv.coords[:, 1:])
+ qkv = qkv.replace(torch.stack([q, k, v], dim=1))
+ return qkv
+
+ def forward(
+ self,
+ x: Union[SparseTensor, torch.Tensor],
+ context: Optional[Union[SparseTensor, torch.Tensor]] = None,
+ ) -> Union[SparseTensor, torch.Tensor]:
+ if self._type == "self":
+ qkv = self._linear(self.to_qkv, x)
+ qkv = self._fused_pre(qkv, num_fused=3)
+ if self.use_rope:
+ qkv = self._rope(qkv)
+ if self.qk_rms_norm:
+ q, k, v = qkv.unbind(dim=1)
+ q = self.q_rms_norm(q)
+ k = self.k_rms_norm(k)
+ qkv = qkv.replace(torch.stack([q.feats, k.feats, v.feats], dim=1))
+ if self.attn_mode == "full":
+ h = sparse_scaled_dot_product_attention(qkv)
+ elif self.attn_mode == "serialized":
+ h = sparse_serialized_scaled_dot_product_self_attention(
+ qkv,
+ self.window_size,
+ serialize_mode=self.serialize_mode,
+ shift_sequence=self.shift_sequence,
+ shift_window=self.shift_window,
+ )
+ elif self.attn_mode == "windowed":
+ h = sparse_windowed_scaled_dot_product_self_attention(
+ qkv, self.window_size, shift_window=self.shift_window
+ )
+ else:
+ q = self._linear(self.to_q, x)
+ q = self._reshape_chs(q, (self.num_heads, -1))
+ kv = self._linear(self.to_kv, context)
+ kv = self._fused_pre(kv, num_fused=2)
+ if self.qk_rms_norm:
+ q = self.q_rms_norm(q)
+ k, v = kv.unbind(dim=1)
+ k = self.k_rms_norm(k)
+ kv = kv.replace(torch.stack([k.feats, v.feats], dim=1))
+ h = sparse_scaled_dot_product_attention(q, kv)
+ h = self._reshape_chs(h, (-1,))
+ h = self._linear(self.to_out, h)
+ return h
diff --git a/deps/vomp/vomp/modules/sparse/attention/serialized_attn.py b/deps/vomp/vomp/modules/sparse/attention/serialized_attn.py
new file mode 100755
index 0000000000000000000000000000000000000000..d1a56dbede44ebfbac2bb6d0b116b145976edddc
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/attention/serialized_attn.py
@@ -0,0 +1,262 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+from enum import Enum
+import torch
+import math
+from .. import SparseTensor
+from .. import DEBUG, ATTN
+
+if ATTN == "xformers":
+ import xformers.ops as xops
+elif ATTN == "flash_attn":
+ import flash_attn
+else:
+ raise ValueError(f"Unknown attention module: {ATTN}")
+
+
+__all__ = [
+ "sparse_serialized_scaled_dot_product_self_attention",
+]
+
+
+class SerializeMode(Enum):
+ Z_ORDER = 0
+ Z_ORDER_TRANSPOSED = 1
+ HILBERT = 2
+ HILBERT_TRANSPOSED = 3
+
+
+SerializeModes = [
+ SerializeMode.Z_ORDER,
+ SerializeMode.Z_ORDER_TRANSPOSED,
+ SerializeMode.HILBERT,
+ SerializeMode.HILBERT_TRANSPOSED,
+]
+
+
+def calc_serialization(
+ tensor: SparseTensor,
+ window_size: int,
+ serialize_mode: SerializeMode = SerializeMode.Z_ORDER,
+ shift_sequence: int = 0,
+ shift_window: Tuple[int, int, int] = (0, 0, 0),
+) -> Tuple[torch.Tensor, torch.Tensor, List[int]]:
+ """
+ Calculate serialization and partitioning for a set of coordinates.
+
+ Args:
+ tensor (SparseTensor): The input tensor.
+ window_size (int): The window size to use.
+ serialize_mode (SerializeMode): The serialization mode to use.
+ shift_sequence (int): The shift of serialized sequence.
+ shift_window (Tuple[int, int, int]): The shift of serialized coordinates.
+
+ Returns:
+ (torch.Tensor, torch.Tensor): Forwards and backwards indices.
+ """
+ fwd_indices = []
+ bwd_indices = []
+ seq_lens = []
+ seq_batch_indices = []
+ offsets = [0]
+
+ if "vox2seq" not in globals():
+ import vox2seq
+
+ # Serialize the input
+ serialize_coords = tensor.coords[:, 1:].clone()
+ serialize_coords += torch.tensor(
+ shift_window, dtype=torch.int32, device=tensor.device
+ ).reshape(1, 3)
+ if serialize_mode == SerializeMode.Z_ORDER:
+ code = vox2seq.encode(serialize_coords, mode="z_order", permute=[0, 1, 2])
+ elif serialize_mode == SerializeMode.Z_ORDER_TRANSPOSED:
+ code = vox2seq.encode(serialize_coords, mode="z_order", permute=[1, 0, 2])
+ elif serialize_mode == SerializeMode.HILBERT:
+ code = vox2seq.encode(serialize_coords, mode="hilbert", permute=[0, 1, 2])
+ elif serialize_mode == SerializeMode.HILBERT_TRANSPOSED:
+ code = vox2seq.encode(serialize_coords, mode="hilbert", permute=[1, 0, 2])
+ else:
+ raise ValueError(f"Unknown serialize mode: {serialize_mode}")
+
+ for bi, s in enumerate(tensor.layout):
+ num_points = s.stop - s.start
+ num_windows = (num_points + window_size - 1) // window_size
+ valid_window_size = num_points / num_windows
+ to_ordered = torch.argsort(code[s.start : s.stop])
+ if num_windows == 1:
+ fwd_indices.append(to_ordered)
+ bwd_indices.append(
+ torch.zeros_like(to_ordered).scatter_(
+ 0, to_ordered, torch.arange(num_points, device=tensor.device)
+ )
+ )
+ fwd_indices[-1] += s.start
+ bwd_indices[-1] += offsets[-1]
+ seq_lens.append(num_points)
+ seq_batch_indices.append(bi)
+ offsets.append(offsets[-1] + seq_lens[-1])
+ else:
+ # Partition the input
+ offset = 0
+ mids = [
+ (i + 0.5) * valid_window_size + shift_sequence
+ for i in range(num_windows)
+ ]
+ split = [
+ math.floor(i * valid_window_size + shift_sequence)
+ for i in range(num_windows + 1)
+ ]
+ bwd_index = torch.zeros(
+ (num_points,), dtype=torch.int64, device=tensor.device
+ )
+ for i in range(num_windows):
+ mid = mids[i]
+ valid_start = split[i]
+ valid_end = split[i + 1]
+ padded_start = math.floor(mid - 0.5 * window_size)
+ padded_end = padded_start + window_size
+ fwd_indices.append(
+ to_ordered[
+ torch.arange(padded_start, padded_end, device=tensor.device)
+ % num_points
+ ]
+ )
+ offset += valid_start - padded_start
+ bwd_index.scatter_(
+ 0,
+ fwd_indices[-1][
+ valid_start - padded_start : valid_end - padded_start
+ ],
+ torch.arange(
+ offset, offset + valid_end - valid_start, device=tensor.device
+ ),
+ )
+ offset += padded_end - valid_start
+ fwd_indices[-1] += s.start
+ seq_lens.extend([window_size] * num_windows)
+ seq_batch_indices.extend([bi] * num_windows)
+ bwd_indices.append(bwd_index + offsets[-1])
+ offsets.append(offsets[-1] + num_windows * window_size)
+
+ fwd_indices = torch.cat(fwd_indices)
+ bwd_indices = torch.cat(bwd_indices)
+
+ return fwd_indices, bwd_indices, seq_lens, seq_batch_indices
+
+
+def sparse_serialized_scaled_dot_product_self_attention(
+ qkv: SparseTensor,
+ window_size: int,
+ serialize_mode: SerializeMode = SerializeMode.Z_ORDER,
+ shift_sequence: int = 0,
+ shift_window: Tuple[int, int, int] = (0, 0, 0),
+) -> SparseTensor:
+ """
+ Apply serialized scaled dot product self attention to a sparse tensor.
+
+ Args:
+ qkv (SparseTensor): [N, *, 3, H, C] sparse tensor containing Qs, Ks, and Vs.
+ window_size (int): The window size to use.
+ serialize_mode (SerializeMode): The serialization mode to use.
+ shift_sequence (int): The shift of serialized sequence.
+ shift_window (Tuple[int, int, int]): The shift of serialized coordinates.
+ shift (int): The shift to use.
+ """
+ assert (
+ len(qkv.shape) == 4 and qkv.shape[1] == 3
+ ), f"Invalid shape for qkv, got {qkv.shape}, expected [N, *, 3, H, C]"
+
+ serialization_spatial_cache_name = (
+ f"serialization_{serialize_mode}_{window_size}_{shift_sequence}_{shift_window}"
+ )
+ serialization_spatial_cache = qkv.get_spatial_cache(
+ serialization_spatial_cache_name
+ )
+ if serialization_spatial_cache is None:
+ fwd_indices, bwd_indices, seq_lens, seq_batch_indices = calc_serialization(
+ qkv, window_size, serialize_mode, shift_sequence, shift_window
+ )
+ qkv.register_spatial_cache(
+ serialization_spatial_cache_name,
+ (fwd_indices, bwd_indices, seq_lens, seq_batch_indices),
+ )
+ else:
+ fwd_indices, bwd_indices, seq_lens, seq_batch_indices = (
+ serialization_spatial_cache
+ )
+
+ M = fwd_indices.shape[0]
+ T = qkv.feats.shape[0]
+ H = qkv.feats.shape[2]
+ C = qkv.feats.shape[3]
+
+ qkv_feats = qkv.feats[fwd_indices] # [M, 3, H, C]
+
+ if DEBUG:
+ start = 0
+ qkv_coords = qkv.coords[fwd_indices]
+ for i in range(len(seq_lens)):
+ assert (
+ qkv_coords[start : start + seq_lens[i], 0] == seq_batch_indices[i]
+ ).all(), (
+ f"SparseWindowedScaledDotProductSelfAttention: batch index mismatch"
+ )
+ start += seq_lens[i]
+
+ if all([seq_len == window_size for seq_len in seq_lens]):
+ B = len(seq_lens)
+ N = window_size
+ qkv_feats = qkv_feats.reshape(B, N, 3, H, C)
+ if ATTN == "xformers":
+ q, k, v = qkv_feats.unbind(dim=2) # [B, N, H, C]
+ out = xops.memory_efficient_attention(q, k, v) # [B, N, H, C]
+ elif ATTN == "flash_attn":
+ out = flash_attn.flash_attn_qkvpacked_func(qkv_feats) # [B, N, H, C]
+ else:
+ raise ValueError(f"Unknown attention module: {ATTN}")
+ out = out.reshape(B * N, H, C) # [M, H, C]
+ else:
+ if ATTN == "xformers":
+ q, k, v = qkv_feats.unbind(dim=1) # [M, H, C]
+ q = q.unsqueeze(0) # [1, M, H, C]
+ k = k.unsqueeze(0) # [1, M, H, C]
+ v = v.unsqueeze(0) # [1, M, H, C]
+ mask = xops.fmha.BlockDiagonalMask.from_seqlens(seq_lens)
+ out = xops.memory_efficient_attention(q, k, v, mask)[0] # [M, H, C]
+ elif ATTN == "flash_attn":
+ cu_seqlens = (
+ torch.cat(
+ [torch.tensor([0]), torch.cumsum(torch.tensor(seq_lens), dim=0)],
+ dim=0,
+ )
+ .to(qkv.device)
+ .int()
+ )
+ out = flash_attn.flash_attn_varlen_qkvpacked_func(
+ qkv_feats, cu_seqlens, max(seq_lens)
+ ) # [M, H, C]
+
+ out = out[bwd_indices] # [T, H, C]
+
+ if DEBUG:
+ qkv_coords = qkv_coords[bwd_indices]
+ assert torch.equal(
+ qkv_coords, qkv.coords
+ ), "SparseWindowedScaledDotProductSelfAttention: coordinate mismatch"
+
+ return qkv.replace(out)
diff --git a/deps/vomp/vomp/modules/sparse/attention/windowed_attn.py b/deps/vomp/vomp/modules/sparse/attention/windowed_attn.py
new file mode 100755
index 0000000000000000000000000000000000000000..8a06ffe04822b1028930cfa64cb9cf9a6f140936
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/attention/windowed_attn.py
@@ -0,0 +1,190 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import math
+from .. import SparseTensor
+from .. import DEBUG, ATTN
+
+if ATTN == "xformers":
+ import xformers.ops as xops
+elif ATTN == "flash_attn":
+ import flash_attn
+else:
+ raise ValueError(f"Unknown attention module: {ATTN}")
+
+
+__all__ = [
+ "sparse_windowed_scaled_dot_product_self_attention",
+]
+
+
+def calc_window_partition(
+ tensor: SparseTensor,
+ window_size: Union[int, Tuple[int, ...]],
+ shift_window: Union[int, Tuple[int, ...]] = 0,
+) -> Tuple[torch.Tensor, torch.Tensor, List[int], List[int]]:
+ """
+ Calculate serialization and partitioning for a set of coordinates.
+
+ Args:
+ tensor (SparseTensor): The input tensor.
+ window_size (int): The window size to use.
+ shift_window (Tuple[int, ...]): The shift of serialized coordinates.
+
+ Returns:
+ (torch.Tensor): Forwards indices.
+ (torch.Tensor): Backwards indices.
+ (List[int]): Sequence lengths.
+ (List[int]): Sequence batch indices.
+ """
+ DIM = tensor.coords.shape[1] - 1
+ shift_window = (
+ (shift_window,) * DIM if isinstance(shift_window, int) else shift_window
+ )
+ window_size = (window_size,) * DIM if isinstance(window_size, int) else window_size
+ shifted_coords = tensor.coords.clone().detach()
+ shifted_coords[:, 1:] += torch.tensor(
+ shift_window, device=tensor.device, dtype=torch.int32
+ ).unsqueeze(0)
+
+ MAX_COORDS = shifted_coords[:, 1:].max(dim=0).values.tolist()
+ NUM_WINDOWS = [math.ceil((mc + 1) / ws) for mc, ws in zip(MAX_COORDS, window_size)]
+ OFFSET = torch.cumprod(torch.tensor([1] + NUM_WINDOWS[::-1]), dim=0).tolist()[::-1]
+
+ shifted_coords[:, 1:] //= torch.tensor(
+ window_size, device=tensor.device, dtype=torch.int32
+ ).unsqueeze(0)
+ shifted_indices = (
+ shifted_coords
+ * torch.tensor(OFFSET, device=tensor.device, dtype=torch.int32).unsqueeze(0)
+ ).sum(dim=1)
+ fwd_indices = torch.argsort(shifted_indices)
+ bwd_indices = torch.empty_like(fwd_indices)
+ bwd_indices[fwd_indices] = torch.arange(fwd_indices.shape[0], device=tensor.device)
+ seq_lens = torch.bincount(shifted_indices)
+ seq_batch_indices = (
+ torch.arange(seq_lens.shape[0], device=tensor.device, dtype=torch.int32)
+ // OFFSET[0]
+ )
+ mask = seq_lens != 0
+ seq_lens = seq_lens[mask].tolist()
+ seq_batch_indices = seq_batch_indices[mask].tolist()
+
+ return fwd_indices, bwd_indices, seq_lens, seq_batch_indices
+
+
+def sparse_windowed_scaled_dot_product_self_attention(
+ qkv: SparseTensor, window_size: int, shift_window: Tuple[int, int, int] = (0, 0, 0)
+) -> SparseTensor:
+ """
+ Apply windowed scaled dot product self attention to a sparse tensor.
+
+ Args:
+ qkv (SparseTensor): [N, *, 3, H, C] sparse tensor containing Qs, Ks, and Vs.
+ window_size (int): The window size to use.
+ shift_window (Tuple[int, int, int]): The shift of serialized coordinates.
+ shift (int): The shift to use.
+ """
+ assert (
+ len(qkv.shape) == 4 and qkv.shape[1] == 3
+ ), f"Invalid shape for qkv, got {qkv.shape}, expected [N, *, 3, H, C]"
+
+ serialization_spatial_cache_name = f"window_partition_{window_size}_{shift_window}"
+ serialization_spatial_cache = qkv.get_spatial_cache(
+ serialization_spatial_cache_name
+ )
+ if serialization_spatial_cache is None:
+ fwd_indices, bwd_indices, seq_lens, seq_batch_indices = calc_window_partition(
+ qkv, window_size, shift_window
+ )
+ qkv.register_spatial_cache(
+ serialization_spatial_cache_name,
+ (fwd_indices, bwd_indices, seq_lens, seq_batch_indices),
+ )
+ else:
+ fwd_indices, bwd_indices, seq_lens, seq_batch_indices = (
+ serialization_spatial_cache
+ )
+
+ M = fwd_indices.shape[0]
+ T = qkv.feats.shape[0]
+ H = qkv.feats.shape[2]
+ C = qkv.feats.shape[3]
+
+ qkv_feats = qkv.feats[fwd_indices] # [M, 3, H, C]
+
+ if DEBUG:
+ start = 0
+ qkv_coords = qkv.coords[fwd_indices]
+ for i in range(len(seq_lens)):
+ seq_coords = qkv_coords[start : start + seq_lens[i]]
+ assert (
+ seq_coords[:, 0] == seq_batch_indices[i]
+ ).all(), (
+ f"SparseWindowedScaledDotProductSelfAttention: batch index mismatch"
+ )
+ assert (
+ seq_coords[:, 1:].max(dim=0).values
+ - seq_coords[:, 1:].min(dim=0).values
+ < window_size
+ ).all(), (
+ f"SparseWindowedScaledDotProductSelfAttention: window size exceeded"
+ )
+ start += seq_lens[i]
+
+ if all([seq_len == window_size for seq_len in seq_lens]):
+ B = len(seq_lens)
+ N = window_size
+ qkv_feats = qkv_feats.reshape(B, N, 3, H, C)
+ if ATTN == "xformers":
+ q, k, v = qkv_feats.unbind(dim=2) # [B, N, H, C]
+ out = xops.memory_efficient_attention(q, k, v) # [B, N, H, C]
+ elif ATTN == "flash_attn":
+ out = flash_attn.flash_attn_qkvpacked_func(qkv_feats) # [B, N, H, C]
+ else:
+ raise ValueError(f"Unknown attention module: {ATTN}")
+ out = out.reshape(B * N, H, C) # [M, H, C]
+ else:
+ if ATTN == "xformers":
+ q, k, v = qkv_feats.unbind(dim=1) # [M, H, C]
+ q = q.unsqueeze(0) # [1, M, H, C]
+ k = k.unsqueeze(0) # [1, M, H, C]
+ v = v.unsqueeze(0) # [1, M, H, C]
+ mask = xops.fmha.BlockDiagonalMask.from_seqlens(seq_lens)
+ out = xops.memory_efficient_attention(q, k, v, mask)[0] # [M, H, C]
+ elif ATTN == "flash_attn":
+ cu_seqlens = (
+ torch.cat(
+ [torch.tensor([0]), torch.cumsum(torch.tensor(seq_lens), dim=0)],
+ dim=0,
+ )
+ .to(qkv.device)
+ .int()
+ )
+ out = flash_attn.flash_attn_varlen_qkvpacked_func(
+ qkv_feats, cu_seqlens, max(seq_lens)
+ ) # [M, H, C]
+
+ out = out[bwd_indices] # [T, H, C]
+
+ if DEBUG:
+ qkv_coords = qkv_coords[bwd_indices]
+ assert torch.equal(
+ qkv_coords, qkv.coords
+ ), "SparseWindowedScaledDotProductSelfAttention: coordinate mismatch"
+
+ return qkv.replace(out)
diff --git a/deps/vomp/vomp/modules/sparse/basic.py b/deps/vomp/vomp/modules/sparse/basic.py
new file mode 100755
index 0000000000000000000000000000000000000000..a9224ee0418d002ca01108921baaef97dc48da87
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/basic.py
@@ -0,0 +1,555 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from . import BACKEND, DEBUG
+
+SparseTensorData = None # Lazy import
+
+
+def _ensure_sparse_tensor_data():
+ """Ensure SparseTensorData is imported"""
+ global SparseTensorData
+ if SparseTensorData is None:
+ import importlib
+
+ if BACKEND == "torchsparse":
+ SparseTensorData = importlib.import_module("torchsparse").SparseTensor
+ elif BACKEND == "spconv":
+ SparseTensorData = importlib.import_module(
+ "spconv.pytorch"
+ ).SparseConvTensor
+
+
+__all__ = [
+ "SparseTensor",
+ "sparse_batch_broadcast",
+ "sparse_batch_op",
+ "sparse_cat",
+ "sparse_unbind",
+]
+
+
+class SparseTensor:
+ """
+ Sparse tensor with support for both torchsparse and spconv backends.
+
+ Parameters:
+ - feats (torch.Tensor): Features of the sparse tensor.
+ - coords (torch.Tensor): Coordinates of the sparse tensor.
+ - shape (torch.Size): Shape of the sparse tensor.
+ - layout (List[slice]): Layout of the sparse tensor for each batch
+ - data (SparseTensorData): Sparse tensor data used for convolusion
+
+ NOTE:
+ - Data corresponding to a same batch should be contiguous.
+ - Coords should be in [0, 1023]
+ """
+
+ @overload
+ def __init__(
+ self,
+ feats: torch.Tensor,
+ coords: torch.Tensor,
+ shape: Optional[torch.Size] = None,
+ layout: Optional[List[slice]] = None,
+ **kwargs,
+ ): ...
+
+ @overload
+ def __init__(
+ self,
+ data,
+ shape: Optional[torch.Size] = None,
+ layout: Optional[List[slice]] = None,
+ **kwargs,
+ ): ...
+
+ def __init__(self, *args, **kwargs):
+ # Lazy import of sparse tensor backend
+ _ensure_sparse_tensor_data()
+ method_id = 0
+ if len(args) != 0:
+ method_id = 0 if isinstance(args[0], torch.Tensor) else 1
+ else:
+ method_id = 1 if "data" in kwargs else 0
+
+ if method_id == 0:
+ feats, coords, shape, layout = args + (None,) * (4 - len(args))
+ if "feats" in kwargs:
+ feats = kwargs["feats"]
+ del kwargs["feats"]
+ if "coords" in kwargs:
+ coords = kwargs["coords"]
+ del kwargs["coords"]
+ if "shape" in kwargs:
+ shape = kwargs["shape"]
+ del kwargs["shape"]
+ if "layout" in kwargs:
+ layout = kwargs["layout"]
+ del kwargs["layout"]
+
+ if shape is None:
+ shape = self.__cal_shape(feats, coords)
+ if layout is None:
+ layout = self.__cal_layout(coords, shape[0])
+ if BACKEND == "torchsparse":
+ self.data = SparseTensorData(feats, coords, **kwargs)
+ elif BACKEND == "spconv":
+ spatial_shape = list(coords.max(0)[0] + 1)[1:]
+ self.data = SparseTensorData(
+ feats.reshape(feats.shape[0], -1),
+ coords,
+ spatial_shape,
+ shape[0],
+ **kwargs,
+ )
+ self.data._features = feats
+ elif method_id == 1:
+ data, shape, layout = args + (None,) * (3 - len(args))
+ if "data" in kwargs:
+ data = kwargs["data"]
+ del kwargs["data"]
+ if "shape" in kwargs:
+ shape = kwargs["shape"]
+ del kwargs["shape"]
+ if "layout" in kwargs:
+ layout = kwargs["layout"]
+ del kwargs["layout"]
+
+ self.data = data
+ if shape is None:
+ shape = self.__cal_shape(self.feats, self.coords)
+ if layout is None:
+ layout = self.__cal_layout(self.coords, shape[0])
+
+ self._shape = shape
+ self._layout = layout
+ self._scale = kwargs.get("scale", (1, 1, 1))
+ self._spatial_cache = kwargs.get("spatial_cache", {})
+
+ if DEBUG:
+ try:
+ assert (
+ self.feats.shape[0] == self.coords.shape[0]
+ ), f"Invalid feats shape: {self.feats.shape}, coords shape: {self.coords.shape}"
+ assert self.shape == self.__cal_shape(
+ self.feats, self.coords
+ ), f"Invalid shape: {self.shape}"
+ assert self.layout == self.__cal_layout(
+ self.coords, self.shape[0]
+ ), f"Invalid layout: {self.layout}"
+ for i in range(self.shape[0]):
+ assert torch.all(
+ self.coords[self.layout[i], 0] == i
+ ), f"The data of batch {i} is not contiguous"
+ except Exception as e:
+ print("Debugging information:")
+ print(f"- Shape: {self.shape}")
+ print(f"- Layout: {self.layout}")
+ print(f"- Scale: {self._scale}")
+ print(f"- Coords: {self.coords}")
+ raise e
+
+ def __cal_shape(self, feats, coords):
+ shape = []
+ shape.append(coords[:, 0].max().item() + 1)
+ shape.extend([*feats.shape[1:]])
+ return torch.Size(shape)
+
+ def __cal_layout(self, coords, batch_size):
+ seq_len = torch.bincount(coords[:, 0], minlength=batch_size)
+ offset = torch.cumsum(seq_len, dim=0)
+ layout = [
+ slice((offset[i] - seq_len[i]).item(), offset[i].item())
+ for i in range(batch_size)
+ ]
+ return layout
+
+ @property
+ def shape(self) -> torch.Size:
+ return self._shape
+
+ def dim(self) -> int:
+ return len(self.shape)
+
+ @property
+ def layout(self) -> List[slice]:
+ return self._layout
+
+ @property
+ def feats(self) -> torch.Tensor:
+ if BACKEND == "torchsparse":
+ return self.data.F
+ elif BACKEND == "spconv":
+ return self.data.features
+
+ @feats.setter
+ def feats(self, value: torch.Tensor):
+ if BACKEND == "torchsparse":
+ self.data.F = value
+ elif BACKEND == "spconv":
+ self.data.features = value
+
+ @property
+ def coords(self) -> torch.Tensor:
+ if BACKEND == "torchsparse":
+ return self.data.C
+ elif BACKEND == "spconv":
+ return self.data.indices
+
+ @coords.setter
+ def coords(self, value: torch.Tensor):
+ if BACKEND == "torchsparse":
+ self.data.C = value
+ elif BACKEND == "spconv":
+ self.data.indices = value
+
+ @property
+ def dtype(self):
+ return self.feats.dtype
+
+ @property
+ def device(self):
+ return self.feats.device
+
+ @overload
+ def to(self, dtype: torch.dtype) -> "SparseTensor": ...
+
+ @overload
+ def to(
+ self,
+ device: Optional[Union[str, torch.device]] = None,
+ dtype: Optional[torch.dtype] = None,
+ ) -> "SparseTensor": ...
+
+ def to(self, *args, **kwargs) -> "SparseTensor":
+ device = None
+ dtype = None
+ if len(args) == 2:
+ device, dtype = args
+ elif len(args) == 1:
+ if isinstance(args[0], torch.dtype):
+ dtype = args[0]
+ else:
+ device = args[0]
+ if "dtype" in kwargs:
+ assert dtype is None, "to() received multiple values for argument 'dtype'"
+ dtype = kwargs["dtype"]
+ if "device" in kwargs:
+ assert device is None, "to() received multiple values for argument 'device'"
+ device = kwargs["device"]
+
+ new_feats = self.feats.to(device=device, dtype=dtype)
+ new_coords = self.coords.to(device=device)
+ return self.replace(new_feats, new_coords)
+
+ def type(self, dtype):
+ new_feats = self.feats.type(dtype)
+ return self.replace(new_feats)
+
+ def cpu(self) -> "SparseTensor":
+ new_feats = self.feats.cpu()
+ new_coords = self.coords.cpu()
+ return self.replace(new_feats, new_coords)
+
+ def cuda(self) -> "SparseTensor":
+ new_feats = self.feats.cuda()
+ new_coords = self.coords.cuda()
+ return self.replace(new_feats, new_coords)
+
+ def half(self) -> "SparseTensor":
+ new_feats = self.feats.half()
+ return self.replace(new_feats)
+
+ def float(self) -> "SparseTensor":
+ new_feats = self.feats.float()
+ return self.replace(new_feats)
+
+ def detach(self) -> "SparseTensor":
+ new_coords = self.coords.detach()
+ new_feats = self.feats.detach()
+ return self.replace(new_feats, new_coords)
+
+ def dense(self) -> torch.Tensor:
+ if BACKEND == "torchsparse":
+ return self.data.dense()
+ elif BACKEND == "spconv":
+ return self.data.dense()
+
+ def reshape(self, *shape) -> "SparseTensor":
+ new_feats = self.feats.reshape(self.feats.shape[0], *shape)
+ return self.replace(new_feats)
+
+ def unbind(self, dim: int) -> List["SparseTensor"]:
+ return sparse_unbind(self, dim)
+
+ def replace(
+ self, feats: torch.Tensor, coords: Optional[torch.Tensor] = None
+ ) -> "SparseTensor":
+ _ensure_sparse_tensor_data()
+ new_shape = [self.shape[0]]
+ new_shape.extend(feats.shape[1:])
+ if BACKEND == "torchsparse":
+ new_data = SparseTensorData(
+ feats=feats,
+ coords=self.data.coords if coords is None else coords,
+ stride=self.data.stride,
+ spatial_range=self.data.spatial_range,
+ )
+ new_data._caches = self.data._caches
+ elif BACKEND == "spconv":
+ new_data = SparseTensorData(
+ self.data.features.reshape(self.data.features.shape[0], -1),
+ self.data.indices,
+ self.data.spatial_shape,
+ self.data.batch_size,
+ self.data.grid,
+ self.data.voxel_num,
+ self.data.indice_dict,
+ )
+ new_data._features = feats
+ new_data.benchmark = self.data.benchmark
+ new_data.benchmark_record = self.data.benchmark_record
+ new_data.thrust_allocator = self.data.thrust_allocator
+ new_data._timer = self.data._timer
+ new_data.force_algo = self.data.force_algo
+ new_data.int8_scale = self.data.int8_scale
+ if coords is not None:
+ new_data.indices = coords
+ new_tensor = SparseTensor(
+ new_data,
+ shape=torch.Size(new_shape),
+ layout=self.layout,
+ scale=self._scale,
+ spatial_cache=self._spatial_cache,
+ )
+ return new_tensor
+
+ @staticmethod
+ def full(aabb, dim, value, dtype=torch.float32, device=None) -> "SparseTensor":
+ N, C = dim
+ x = torch.arange(aabb[0], aabb[3] + 1)
+ y = torch.arange(aabb[1], aabb[4] + 1)
+ z = torch.arange(aabb[2], aabb[5] + 1)
+ coords = torch.stack(torch.meshgrid(x, y, z, indexing="ij"), dim=-1).reshape(
+ -1, 3
+ )
+ coords = torch.cat(
+ [
+ torch.arange(N).view(-1, 1).repeat(1, coords.shape[0]).view(-1, 1),
+ coords.repeat(N, 1),
+ ],
+ dim=1,
+ ).to(dtype=torch.int32, device=device)
+ feats = torch.full((coords.shape[0], C), value, dtype=dtype, device=device)
+ return SparseTensor(feats=feats, coords=coords)
+
+ def __merge_sparse_cache(self, other: "SparseTensor") -> dict:
+ new_cache = {}
+ for k in set(
+ list(self._spatial_cache.keys()) + list(other._spatial_cache.keys())
+ ):
+ if k in self._spatial_cache:
+ new_cache[k] = self._spatial_cache[k]
+ if k in other._spatial_cache:
+ if k not in new_cache:
+ new_cache[k] = other._spatial_cache[k]
+ else:
+ new_cache[k].update(other._spatial_cache[k])
+ return new_cache
+
+ def __neg__(self) -> "SparseTensor":
+ return self.replace(-self.feats)
+
+ def __elemwise__(
+ self, other: Union[torch.Tensor, "SparseTensor"], op: callable
+ ) -> "SparseTensor":
+ if isinstance(other, torch.Tensor):
+ try:
+ other = torch.broadcast_to(other, self.shape)
+ other = sparse_batch_broadcast(self, other)
+ except:
+ pass
+ if isinstance(other, SparseTensor):
+ other = other.feats
+ new_feats = op(self.feats, other)
+ new_tensor = self.replace(new_feats)
+ if isinstance(other, SparseTensor):
+ new_tensor._spatial_cache = self.__merge_sparse_cache(other)
+ return new_tensor
+
+ def __add__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, torch.add)
+
+ def __radd__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, torch.add)
+
+ def __sub__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, torch.sub)
+
+ def __rsub__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, lambda x, y: torch.sub(y, x))
+
+ def __mul__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, torch.mul)
+
+ def __rmul__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, torch.mul)
+
+ def __truediv__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, torch.div)
+
+ def __rtruediv__(
+ self, other: Union[torch.Tensor, "SparseTensor", float]
+ ) -> "SparseTensor":
+ return self.__elemwise__(other, lambda x, y: torch.div(y, x))
+
+ def __getitem__(self, idx):
+ if isinstance(idx, int):
+ idx = [idx]
+ elif isinstance(idx, slice):
+ idx = range(*idx.indices(self.shape[0]))
+ elif isinstance(idx, torch.Tensor):
+ if idx.dtype == torch.bool:
+ assert idx.shape == (
+ self.shape[0],
+ ), f"Invalid index shape: {idx.shape}"
+ idx = idx.nonzero().squeeze(1)
+ elif idx.dtype in [torch.int32, torch.int64]:
+ assert len(idx.shape) == 1, f"Invalid index shape: {idx.shape}"
+ else:
+ raise ValueError(f"Unknown index type: {idx.dtype}")
+ else:
+ raise ValueError(f"Unknown index type: {type(idx)}")
+
+ coords = []
+ feats = []
+ for new_idx, old_idx in enumerate(idx):
+ coords.append(self.coords[self.layout[old_idx]].clone())
+ coords[-1][:, 0] = new_idx
+ feats.append(self.feats[self.layout[old_idx]])
+ coords = torch.cat(coords, dim=0).contiguous()
+ feats = torch.cat(feats, dim=0).contiguous()
+ return SparseTensor(feats=feats, coords=coords)
+
+ def register_spatial_cache(self, key, value) -> None:
+ """
+ Register a spatial cache.
+ The spatial cache can be any thing you want to cache.
+ The registery and retrieval of the cache is based on current scale.
+ """
+ scale_key = str(self._scale)
+ if scale_key not in self._spatial_cache:
+ self._spatial_cache[scale_key] = {}
+ self._spatial_cache[scale_key][key] = value
+
+ def get_spatial_cache(self, key=None):
+ """
+ Get a spatial cache.
+ """
+ scale_key = str(self._scale)
+ cur_scale_cache = self._spatial_cache.get(scale_key, {})
+ if key is None:
+ return cur_scale_cache
+ return cur_scale_cache.get(key, None)
+
+
+def sparse_batch_broadcast(input: SparseTensor, other: torch.Tensor) -> torch.Tensor:
+ """
+ Broadcast a 1D tensor to a sparse tensor along the batch dimension then perform an operation.
+
+ Args:
+ input (torch.Tensor): 1D tensor to broadcast.
+ target (SparseTensor): Sparse tensor to broadcast to.
+ op (callable): Operation to perform after broadcasting. Defaults to torch.add.
+ """
+ coords, feats = input.coords, input.feats
+ broadcasted = torch.zeros_like(feats)
+ for k in range(input.shape[0]):
+ broadcasted[input.layout[k]] = other[k]
+ return broadcasted
+
+
+def sparse_batch_op(
+ input: SparseTensor, other: torch.Tensor, op: callable = torch.add
+) -> SparseTensor:
+ """
+ Broadcast a 1D tensor to a sparse tensor along the batch dimension then perform an operation.
+
+ Args:
+ input (torch.Tensor): 1D tensor to broadcast.
+ target (SparseTensor): Sparse tensor to broadcast to.
+ op (callable): Operation to perform after broadcasting. Defaults to torch.add.
+ """
+ return input.replace(op(input.feats, sparse_batch_broadcast(input, other)))
+
+
+def sparse_cat(inputs: List[SparseTensor], dim: int = 0) -> SparseTensor:
+ """
+ Concatenate a list of sparse tensors.
+
+ Args:
+ inputs (List[SparseTensor]): List of sparse tensors to concatenate.
+ """
+ if dim == 0:
+ start = 0
+ coords = []
+ for input in inputs:
+ coords.append(input.coords.clone())
+ coords[-1][:, 0] += start
+ start += input.shape[0]
+ coords = torch.cat(coords, dim=0)
+ feats = torch.cat([input.feats for input in inputs], dim=0)
+ output = SparseTensor(
+ coords=coords,
+ feats=feats,
+ )
+ else:
+ feats = torch.cat([input.feats for input in inputs], dim=dim)
+ output = inputs[0].replace(feats)
+
+ return output
+
+
+def sparse_unbind(input: SparseTensor, dim: int) -> List[SparseTensor]:
+ """
+ Unbind a sparse tensor along a dimension.
+
+ Args:
+ input (SparseTensor): Sparse tensor to unbind.
+ dim (int): Dimension to unbind.
+ """
+ if dim == 0:
+ return [input[i] for i in range(input.shape[0])]
+ else:
+ feats = input.feats.unbind(dim)
+ return [input.replace(f) for f in feats]
diff --git a/deps/vomp/vomp/modules/sparse/conv/__init__.py b/deps/vomp/vomp/modules/sparse/conv/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..56730d5aad513cebcc9cf7f9a7e26caa6be07e40
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/conv/__init__.py
@@ -0,0 +1,40 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .. import BACKEND
+
+SPCONV_ALGO = "auto" # 'auto', 'implicit_gemm', 'native'
+
+
+def __from_env():
+ import os
+
+ global SPCONV_ALGO
+ env_spconv_algo = os.environ.get("SPCONV_ALGO")
+ if env_spconv_algo is not None and env_spconv_algo in [
+ "auto",
+ "implicit_gemm",
+ "native",
+ ]:
+ SPCONV_ALGO = env_spconv_algo
+ print(f"[SPARSE][CONV] spconv algo: {SPCONV_ALGO}")
+
+
+__from_env()
+
+if BACKEND == "torchsparse":
+ from .conv_torchsparse import *
+elif BACKEND == "spconv":
+ from .conv_spconv import *
diff --git a/deps/vomp/vomp/modules/sparse/conv/conv_spconv.py b/deps/vomp/vomp/modules/sparse/conv/conv_spconv.py
new file mode 100755
index 0000000000000000000000000000000000000000..92c089b6c49709beb829825c8d259d8d976c5e5a
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/conv/conv_spconv.py
@@ -0,0 +1,153 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn as nn
+from .. import SparseTensor
+from .. import DEBUG
+from . import SPCONV_ALGO
+
+
+class SparseConv3d(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride=1,
+ dilation=1,
+ padding=None,
+ bias=True,
+ indice_key=None,
+ ):
+ super(SparseConv3d, self).__init__()
+ if "spconv" not in globals():
+ import spconv.pytorch as spconv
+ algo = None
+ if SPCONV_ALGO == "native":
+ algo = spconv.ConvAlgo.Native
+ elif SPCONV_ALGO == "implicit_gemm":
+ algo = spconv.ConvAlgo.MaskImplicitGemm
+ if stride == 1 and (padding is None):
+ self.conv = spconv.SubMConv3d(
+ in_channels,
+ out_channels,
+ kernel_size,
+ dilation=dilation,
+ bias=bias,
+ indice_key=indice_key,
+ algo=algo,
+ )
+ else:
+ self.conv = spconv.SparseConv3d(
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride=stride,
+ dilation=dilation,
+ padding=padding,
+ bias=bias,
+ indice_key=indice_key,
+ algo=algo,
+ )
+ self.stride = (
+ tuple(stride)
+ if isinstance(stride, (list, tuple))
+ else (stride, stride, stride)
+ )
+ self.padding = padding
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ spatial_changed = any(s != 1 for s in self.stride) or (self.padding is not None)
+ new_data = self.conv(x.data)
+ new_shape = [x.shape[0], self.conv.out_channels]
+ new_layout = None if spatial_changed else x.layout
+
+ if spatial_changed and (x.shape[0] != 1):
+ # spconv was non-1 stride will break the contiguous of the output tensor, sort by the coords
+ fwd = new_data.indices[:, 0].argsort()
+ bwd = torch.zeros_like(fwd).scatter_(
+ 0, fwd, torch.arange(fwd.shape[0], device=fwd.device)
+ )
+ sorted_feats = new_data.features[fwd]
+ sorted_coords = new_data.indices[fwd]
+ unsorted_data = new_data
+ new_data = spconv.SparseConvTensor(sorted_feats, sorted_coords, unsorted_data.spatial_shape, unsorted_data.batch_size) # type: ignore
+
+ out = SparseTensor(
+ new_data,
+ shape=torch.Size(new_shape),
+ layout=new_layout,
+ scale=tuple([s * stride for s, stride in zip(x._scale, self.stride)]),
+ spatial_cache=x._spatial_cache,
+ )
+
+ if spatial_changed and (x.shape[0] != 1):
+ out.register_spatial_cache(
+ f"conv_{self.stride}_unsorted_data", unsorted_data
+ )
+ out.register_spatial_cache(f"conv_{self.stride}_sort_bwd", bwd)
+
+ return out
+
+
+class SparseInverseConv3d(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride=1,
+ dilation=1,
+ bias=True,
+ indice_key=None,
+ ):
+ super(SparseInverseConv3d, self).__init__()
+ if "spconv" not in globals():
+ import spconv.pytorch as spconv
+ self.conv = spconv.SparseInverseConv3d(
+ in_channels, out_channels, kernel_size, bias=bias, indice_key=indice_key
+ )
+ self.stride = (
+ tuple(stride)
+ if isinstance(stride, (list, tuple))
+ else (stride, stride, stride)
+ )
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ spatial_changed = any(s != 1 for s in self.stride)
+ if spatial_changed:
+ # recover the original spconv order
+ data = x.get_spatial_cache(f"conv_{self.stride}_unsorted_data")
+ bwd = x.get_spatial_cache(f"conv_{self.stride}_sort_bwd")
+ data = data.replace_feature(x.feats[bwd])
+ if DEBUG:
+ assert torch.equal(
+ data.indices, x.coords[bwd]
+ ), "Recover the original order failed"
+ else:
+ data = x.data
+
+ new_data = self.conv(data)
+ new_shape = [x.shape[0], self.conv.out_channels]
+ new_layout = None if spatial_changed else x.layout
+ out = SparseTensor(
+ new_data,
+ shape=torch.Size(new_shape),
+ layout=new_layout,
+ scale=tuple([s // stride for s, stride in zip(x._scale, self.stride)]),
+ spatial_cache=x._spatial_cache,
+ )
+ return out
diff --git a/deps/vomp/vomp/modules/sparse/conv/conv_torchsparse.py b/deps/vomp/vomp/modules/sparse/conv/conv_torchsparse.py
new file mode 100755
index 0000000000000000000000000000000000000000..6f52fbab1d124dde5135c74da908b2a8b7d84f95
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/conv/conv_torchsparse.py
@@ -0,0 +1,91 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn as nn
+from .. import SparseTensor
+
+
+class SparseConv3d(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride=1,
+ dilation=1,
+ bias=True,
+ indice_key=None,
+ ):
+ super(SparseConv3d, self).__init__()
+ if "torchsparse" not in globals():
+ import torchsparse
+ self.conv = torchsparse.nn.Conv3d(
+ in_channels, out_channels, kernel_size, stride, 0, dilation, bias
+ )
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ out = self.conv(x.data)
+ new_shape = [x.shape[0], self.conv.out_channels]
+ out = SparseTensor(
+ out,
+ shape=torch.Size(new_shape),
+ layout=x.layout if all(s == 1 for s in self.conv.stride) else None,
+ )
+ out._spatial_cache = x._spatial_cache
+ out._scale = tuple(
+ [s * stride for s, stride in zip(x._scale, self.conv.stride)]
+ )
+ return out
+
+
+class SparseInverseConv3d(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride=1,
+ dilation=1,
+ bias=True,
+ indice_key=None,
+ ):
+ super(SparseInverseConv3d, self).__init__()
+ if "torchsparse" not in globals():
+ import torchsparse
+ self.conv = torchsparse.nn.Conv3d(
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride,
+ 0,
+ dilation,
+ bias,
+ transposed=True,
+ )
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ out = self.conv(x.data)
+ new_shape = [x.shape[0], self.conv.out_channels]
+ out = SparseTensor(
+ out,
+ shape=torch.Size(new_shape),
+ layout=x.layout if all(s == 1 for s in self.conv.stride) else None,
+ )
+ out._spatial_cache = x._spatial_cache
+ out._scale = tuple(
+ [s // stride for s, stride in zip(x._scale, self.conv.stride)]
+ )
+ return out
diff --git a/deps/vomp/vomp/modules/sparse/linear.py b/deps/vomp/vomp/modules/sparse/linear.py
new file mode 100755
index 0000000000000000000000000000000000000000..3f98bf67260f64282fe7570edad2d6127a08b9ac
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/linear.py
@@ -0,0 +1,28 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn as nn
+from . import SparseTensor
+
+__all__ = ["SparseLinear"]
+
+
+class SparseLinear(nn.Linear):
+ def __init__(self, in_features, out_features, bias=True):
+ super(SparseLinear, self).__init__(in_features, out_features, bias)
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ return input.replace(super().forward(input.feats))
diff --git a/deps/vomp/vomp/modules/sparse/nonlinearity.py b/deps/vomp/vomp/modules/sparse/nonlinearity.py
new file mode 100755
index 0000000000000000000000000000000000000000..488bc2e49a8082a71859bc569c68f2b5dfa53067
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/nonlinearity.py
@@ -0,0 +1,44 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn as nn
+from . import SparseTensor
+
+__all__ = ["SparseReLU", "SparseSiLU", "SparseGELU", "SparseActivation"]
+
+
+class SparseReLU(nn.ReLU):
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ return input.replace(super().forward(input.feats))
+
+
+class SparseSiLU(nn.SiLU):
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ return input.replace(super().forward(input.feats))
+
+
+class SparseGELU(nn.GELU):
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ return input.replace(super().forward(input.feats))
+
+
+class SparseActivation(nn.Module):
+ def __init__(self, activation: nn.Module):
+ super().__init__()
+ self.activation = activation
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ return input.replace(self.activation(input.feats))
diff --git a/deps/vomp/vomp/modules/sparse/norm.py b/deps/vomp/vomp/modules/sparse/norm.py
new file mode 100755
index 0000000000000000000000000000000000000000..435a246e9570a9116c0d4f9b35f59c84fa965b9d
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/norm.py
@@ -0,0 +1,78 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn as nn
+from . import SparseTensor
+from . import DEBUG
+
+__all__ = [
+ "SparseGroupNorm",
+ "SparseLayerNorm",
+ "SparseGroupNorm32",
+ "SparseLayerNorm32",
+]
+
+
+class SparseGroupNorm(nn.GroupNorm):
+ def __init__(self, num_groups, num_channels, eps=1e-5, affine=True):
+ super(SparseGroupNorm, self).__init__(num_groups, num_channels, eps, affine)
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ nfeats = torch.zeros_like(input.feats)
+ for k in range(input.shape[0]):
+ if DEBUG:
+ assert (
+ input.coords[input.layout[k], 0] == k
+ ).all(), f"SparseGroupNorm: batch index mismatch"
+ bfeats = input.feats[input.layout[k]]
+ bfeats = bfeats.permute(1, 0).reshape(1, input.shape[1], -1)
+ bfeats = super().forward(bfeats)
+ bfeats = bfeats.reshape(input.shape[1], -1).permute(1, 0)
+ nfeats[input.layout[k]] = bfeats
+ return input.replace(nfeats)
+
+
+class SparseLayerNorm(nn.LayerNorm):
+ def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):
+ super(SparseLayerNorm, self).__init__(normalized_shape, eps, elementwise_affine)
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ nfeats = torch.zeros_like(input.feats)
+ for k in range(input.shape[0]):
+ bfeats = input.feats[input.layout[k]]
+ bfeats = bfeats.permute(1, 0).reshape(1, input.shape[1], -1)
+ bfeats = super().forward(bfeats)
+ bfeats = bfeats.reshape(input.shape[1], -1).permute(1, 0)
+ nfeats[input.layout[k]] = bfeats
+ return input.replace(nfeats)
+
+
+class SparseGroupNorm32(SparseGroupNorm):
+ """
+ A GroupNorm layer that converts to float32 before the forward pass.
+ """
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ return super().forward(x.float()).type(x.dtype)
+
+
+class SparseLayerNorm32(SparseLayerNorm):
+ """
+ A LayerNorm layer that converts to float32 before the forward pass.
+ """
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ return super().forward(x.float()).type(x.dtype)
diff --git a/deps/vomp/vomp/modules/sparse/spatial.py b/deps/vomp/vomp/modules/sparse/spatial.py
new file mode 100755
index 0000000000000000000000000000000000000000..95bfe361058f43c49cd3412778b118e28e233d2b
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/spatial.py
@@ -0,0 +1,145 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from . import SparseTensor
+
+__all__ = ["SparseDownsample", "SparseUpsample", "SparseSubdivide"]
+
+
+class SparseDownsample(nn.Module):
+ """
+ Downsample a sparse tensor by a factor of `factor`.
+ Implemented as average pooling.
+ """
+
+ def __init__(self, factor: Union[int, Tuple[int, ...], List[int]]):
+ super(SparseDownsample, self).__init__()
+ self.factor = tuple(factor) if isinstance(factor, (list, tuple)) else factor
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ DIM = input.coords.shape[-1] - 1
+ factor = self.factor if isinstance(self.factor, tuple) else (self.factor,) * DIM
+ assert DIM == len(
+ factor
+ ), "Input coordinates must have the same dimension as the downsample factor."
+
+ coord = list(input.coords.unbind(dim=-1))
+ for i, f in enumerate(factor):
+ coord[i + 1] = coord[i + 1] // f
+
+ MAX = [coord[i + 1].max().item() + 1 for i in range(DIM)]
+ OFFSET = torch.cumprod(torch.tensor(MAX[::-1]), 0).tolist()[::-1] + [1]
+ code = sum([c * o for c, o in zip(coord, OFFSET)])
+ code, idx = code.unique(return_inverse=True)
+
+ new_feats = torch.scatter_reduce(
+ torch.zeros(
+ code.shape[0],
+ input.feats.shape[1],
+ device=input.feats.device,
+ dtype=input.feats.dtype,
+ ),
+ dim=0,
+ index=idx.unsqueeze(1).expand(-1, input.feats.shape[1]),
+ src=input.feats,
+ reduce="mean",
+ )
+ new_coords = torch.stack(
+ [code // OFFSET[0]]
+ + [(code // OFFSET[i + 1]) % MAX[i] for i in range(DIM)],
+ dim=-1,
+ )
+ out = SparseTensor(
+ new_feats,
+ new_coords,
+ input.shape,
+ )
+ out._scale = tuple([s // f for s, f in zip(input._scale, factor)])
+ out._spatial_cache = input._spatial_cache
+
+ out.register_spatial_cache(f"upsample_{factor}_coords", input.coords)
+ out.register_spatial_cache(f"upsample_{factor}_layout", input.layout)
+ out.register_spatial_cache(f"upsample_{factor}_idx", idx)
+
+ return out
+
+
+class SparseUpsample(nn.Module):
+ """
+ Upsample a sparse tensor by a factor of `factor`.
+ Implemented as nearest neighbor interpolation.
+ """
+
+ def __init__(self, factor: Union[int, Tuple[int, int, int], List[int]]):
+ super(SparseUpsample, self).__init__()
+ self.factor = tuple(factor) if isinstance(factor, (list, tuple)) else factor
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ DIM = input.coords.shape[-1] - 1
+ factor = self.factor if isinstance(self.factor, tuple) else (self.factor,) * DIM
+ assert DIM == len(
+ factor
+ ), "Input coordinates must have the same dimension as the upsample factor."
+
+ new_coords = input.get_spatial_cache(f"upsample_{factor}_coords")
+ new_layout = input.get_spatial_cache(f"upsample_{factor}_layout")
+ idx = input.get_spatial_cache(f"upsample_{factor}_idx")
+ if any([x is None for x in [new_coords, new_layout, idx]]):
+ raise ValueError(
+ "Upsample cache not found. SparseUpsample must be paired with SparseDownsample."
+ )
+ new_feats = input.feats[idx]
+ out = SparseTensor(new_feats, new_coords, input.shape, new_layout)
+ out._scale = tuple([s * f for s, f in zip(input._scale, factor)])
+ out._spatial_cache = input._spatial_cache
+ return out
+
+
+class SparseSubdivide(nn.Module):
+ """
+ Upsample a sparse tensor by a factor of `factor`.
+ Implemented as nearest neighbor interpolation.
+ """
+
+ def __init__(self):
+ super(SparseSubdivide, self).__init__()
+
+ def forward(self, input: SparseTensor) -> SparseTensor:
+ DIM = input.coords.shape[-1] - 1
+ # upsample scale=2^DIM
+ n_cube = torch.ones([2] * DIM, device=input.device, dtype=torch.int)
+ n_coords = torch.nonzero(n_cube)
+ n_coords = torch.cat([torch.zeros_like(n_coords[:, :1]), n_coords], dim=-1)
+ factor = n_coords.shape[0]
+ assert factor == 2**DIM
+ # print(n_coords.shape)
+ new_coords = input.coords.clone()
+ new_coords[:, 1:] *= 2
+ new_coords = new_coords.unsqueeze(1) + n_coords.unsqueeze(0).to(
+ new_coords.dtype
+ )
+
+ new_feats = input.feats.unsqueeze(1).expand(
+ input.feats.shape[0], factor, *input.feats.shape[1:]
+ )
+ out = SparseTensor(
+ new_feats.flatten(0, 1), new_coords.flatten(0, 1), input.shape
+ )
+ out._scale = input._scale * 2
+ out._spatial_cache = input._spatial_cache
+ return out
diff --git a/deps/vomp/vomp/modules/sparse/transformer/__init__.py b/deps/vomp/vomp/modules/sparse/transformer/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf34be7595eff566921258bc1e12fd9accf2042e
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/transformer/__init__.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .blocks import *
+from .modulated import *
diff --git a/deps/vomp/vomp/modules/sparse/transformer/blocks.py b/deps/vomp/vomp/modules/sparse/transformer/blocks.py
new file mode 100644
index 0000000000000000000000000000000000000000..90904632adc14c2a436e70d20512e6d4f247841f
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/transformer/blocks.py
@@ -0,0 +1,176 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from ..basic import SparseTensor
+from ..linear import SparseLinear
+from ..nonlinearity import SparseGELU
+from ..attention import SparseMultiHeadAttention, SerializeMode
+from ...norm import LayerNorm32
+
+
+class SparseFeedForwardNet(nn.Module):
+ def __init__(self, channels: int, mlp_ratio: float = 4.0):
+ super().__init__()
+ self.mlp = nn.Sequential(
+ SparseLinear(channels, int(channels * mlp_ratio)),
+ SparseGELU(approximate="tanh"),
+ SparseLinear(int(channels * mlp_ratio), channels),
+ )
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ return self.mlp(x)
+
+
+class SparseTransformerBlock(nn.Module):
+ """
+ Sparse Transformer block (MSA + FFN).
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "full",
+ window_size: Optional[int] = None,
+ shift_sequence: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ serialize_mode: Optional[SerializeMode] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qkv_bias: bool = True,
+ ln_affine: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.norm1 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.attn = SparseMultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_sequence=shift_sequence,
+ shift_window=shift_window,
+ serialize_mode=serialize_mode,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.mlp = SparseFeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+
+ def _forward(self, x: SparseTensor) -> SparseTensor:
+ h = x.replace(self.norm1(x.feats))
+ h = self.attn(h)
+ x = x + h
+ h = x.replace(self.norm2(x.feats))
+ h = self.mlp(h)
+ x = x + h
+ return x
+
+ def forward(self, x: SparseTensor) -> SparseTensor:
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, use_reentrant=False
+ )
+ else:
+ return self._forward(x)
+
+
+class SparseTransformerCrossBlock(nn.Module):
+ """
+ Sparse Transformer cross-attention block (MSA + MCA + FFN).
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ ctx_channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "full",
+ window_size: Optional[int] = None,
+ shift_sequence: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ serialize_mode: Optional[SerializeMode] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qk_rms_norm_cross: bool = False,
+ qkv_bias: bool = True,
+ ln_affine: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.norm1 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.norm3 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.self_attn = SparseMultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ type="self",
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_sequence=shift_sequence,
+ shift_window=shift_window,
+ serialize_mode=serialize_mode,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.cross_attn = SparseMultiHeadAttention(
+ channels,
+ ctx_channels=ctx_channels,
+ num_heads=num_heads,
+ type="cross",
+ attn_mode="full",
+ qkv_bias=qkv_bias,
+ qk_rms_norm=qk_rms_norm_cross,
+ )
+ self.mlp = SparseFeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+
+ def _forward(self, x: SparseTensor, mod: torch.Tensor, context: torch.Tensor):
+ h = x.replace(self.norm1(x.feats))
+ h = self.self_attn(h)
+ x = x + h
+ h = x.replace(self.norm2(x.feats))
+ h = self.cross_attn(h, context)
+ x = x + h
+ h = x.replace(self.norm3(x.feats))
+ h = self.mlp(h)
+ x = x + h
+ return x
+
+ def forward(self, x: SparseTensor, context: torch.Tensor):
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, context, use_reentrant=False
+ )
+ else:
+ return self._forward(x, context)
diff --git a/deps/vomp/vomp/modules/sparse/transformer/modulated.py b/deps/vomp/vomp/modules/sparse/transformer/modulated.py
new file mode 100644
index 0000000000000000000000000000000000000000..657923efbbfd278c0eb416100c58b4671ca26936
--- /dev/null
+++ b/deps/vomp/vomp/modules/sparse/transformer/modulated.py
@@ -0,0 +1,200 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from ..basic import SparseTensor
+from ..attention import SparseMultiHeadAttention, SerializeMode
+from ...norm import LayerNorm32
+from .blocks import SparseFeedForwardNet
+
+
+class ModulatedSparseTransformerBlock(nn.Module):
+ """
+ Sparse Transformer block (MSA + FFN) with adaptive layer norm conditioning.
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "full",
+ window_size: Optional[int] = None,
+ shift_sequence: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ serialize_mode: Optional[SerializeMode] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qkv_bias: bool = True,
+ share_mod: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.share_mod = share_mod
+ self.norm1 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.attn = SparseMultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_sequence=shift_sequence,
+ shift_window=shift_window,
+ serialize_mode=serialize_mode,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.mlp = SparseFeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+ if not share_mod:
+ self.adaLN_modulation = nn.Sequential(
+ nn.SiLU(), nn.Linear(channels, 6 * channels, bias=True)
+ )
+
+ def _forward(self, x: SparseTensor, mod: torch.Tensor) -> SparseTensor:
+ if self.share_mod:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(
+ 6, dim=1
+ )
+ else:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.adaLN_modulation(mod).chunk(6, dim=1)
+ )
+ h = x.replace(self.norm1(x.feats))
+ h = h * (1 + scale_msa) + shift_msa
+ h = self.attn(h)
+ h = h * gate_msa
+ x = x + h
+ h = x.replace(self.norm2(x.feats))
+ h = h * (1 + scale_mlp) + shift_mlp
+ h = self.mlp(h)
+ h = h * gate_mlp
+ x = x + h
+ return x
+
+ def forward(self, x: SparseTensor, mod: torch.Tensor) -> SparseTensor:
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, mod, use_reentrant=False
+ )
+ else:
+ return self._forward(x, mod)
+
+
+class ModulatedSparseTransformerCrossBlock(nn.Module):
+ """
+ Sparse Transformer cross-attention block (MSA + MCA + FFN) with adaptive layer norm conditioning.
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ ctx_channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal[
+ "full", "shift_window", "shift_sequence", "shift_order", "swin"
+ ] = "full",
+ window_size: Optional[int] = None,
+ shift_sequence: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ serialize_mode: Optional[SerializeMode] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qk_rms_norm_cross: bool = False,
+ qkv_bias: bool = True,
+ share_mod: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.share_mod = share_mod
+ self.norm1 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=True, eps=1e-6)
+ self.norm3 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.self_attn = SparseMultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ type="self",
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_sequence=shift_sequence,
+ shift_window=shift_window,
+ serialize_mode=serialize_mode,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.cross_attn = SparseMultiHeadAttention(
+ channels,
+ ctx_channels=ctx_channels,
+ num_heads=num_heads,
+ type="cross",
+ attn_mode="full",
+ qkv_bias=qkv_bias,
+ qk_rms_norm=qk_rms_norm_cross,
+ )
+ self.mlp = SparseFeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+ if not share_mod:
+ self.adaLN_modulation = nn.Sequential(
+ nn.SiLU(), nn.Linear(channels, 6 * channels, bias=True)
+ )
+
+ def _forward(
+ self, x: SparseTensor, mod: torch.Tensor, context: torch.Tensor
+ ) -> SparseTensor:
+ if self.share_mod:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(
+ 6, dim=1
+ )
+ else:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.adaLN_modulation(mod).chunk(6, dim=1)
+ )
+ h = x.replace(self.norm1(x.feats))
+ h = h * (1 + scale_msa) + shift_msa
+ h = self.self_attn(h)
+ h = h * gate_msa
+ x = x + h
+ h = x.replace(self.norm2(x.feats))
+ h = self.cross_attn(h, context)
+ x = x + h
+ h = x.replace(self.norm3(x.feats))
+ h = h * (1 + scale_mlp) + shift_mlp
+ h = self.mlp(h)
+ h = h * gate_mlp
+ x = x + h
+ return x
+
+ def forward(
+ self, x: SparseTensor, mod: torch.Tensor, context: torch.Tensor
+ ) -> SparseTensor:
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, mod, context, use_reentrant=False
+ )
+ else:
+ return self._forward(x, mod, context)
diff --git a/deps/vomp/vomp/modules/spatial.py b/deps/vomp/vomp/modules/spatial.py
new file mode 100644
index 0000000000000000000000000000000000000000..772d5f51516b7c1f833370dc0d6da3f621c2b03c
--- /dev/null
+++ b/deps/vomp/vomp/modules/spatial.py
@@ -0,0 +1,79 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+
+
+def pixel_shuffle_3d(x: torch.Tensor, scale_factor: int) -> torch.Tensor:
+ """
+ 3D pixel shuffle.
+ """
+ B, C, H, W, D = x.shape
+ C_ = C // scale_factor**3
+ x = x.reshape(B, C_, scale_factor, scale_factor, scale_factor, H, W, D)
+ x = x.permute(0, 1, 5, 2, 6, 3, 7, 4)
+ x = x.reshape(B, C_, H * scale_factor, W * scale_factor, D * scale_factor)
+ return x
+
+
+def patchify(x: torch.Tensor, patch_size: int):
+ """
+ Patchify a tensor.
+
+ Args:
+ x (torch.Tensor): (N, C, *spatial) tensor
+ patch_size (int): Patch size
+ """
+ DIM = x.dim() - 2
+ for d in range(2, DIM + 2):
+ assert (
+ x.shape[d] % patch_size == 0
+ ), f"Dimension {d} of input tensor must be divisible by patch size, got {x.shape[d]} and {patch_size}"
+
+ x = x.reshape(
+ *x.shape[:2],
+ *sum([[x.shape[d] // patch_size, patch_size] for d in range(2, DIM + 2)], []),
+ )
+ x = x.permute(
+ 0, 1, *([2 * i + 3 for i in range(DIM)] + [2 * i + 2 for i in range(DIM)])
+ )
+ x = x.reshape(x.shape[0], x.shape[1] * (patch_size**DIM), *(x.shape[-DIM:]))
+ return x
+
+
+def unpatchify(x: torch.Tensor, patch_size: int):
+ """
+ Unpatchify a tensor.
+
+ Args:
+ x (torch.Tensor): (N, C, *spatial) tensor
+ patch_size (int): Patch size
+ """
+ DIM = x.dim() - 2
+ assert (
+ x.shape[1] % (patch_size**DIM) == 0
+ ), f"Second dimension of input tensor must be divisible by patch size to unpatchify, got {x.shape[1]} and {patch_size ** DIM}"
+
+ x = x.reshape(
+ x.shape[0],
+ x.shape[1] // (patch_size**DIM),
+ *([patch_size] * DIM),
+ *(x.shape[-DIM:]),
+ )
+ x = x.permute(0, 1, *(sum([[2 + DIM + i, 2 + i] for i in range(DIM)], [])))
+ x = x.reshape(
+ x.shape[0], x.shape[1], *[x.shape[2 + 2 * i] * patch_size for i in range(DIM)]
+ )
+ return x
diff --git a/deps/vomp/vomp/modules/transformer/__init__.py b/deps/vomp/vomp/modules/transformer/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf34be7595eff566921258bc1e12fd9accf2042e
--- /dev/null
+++ b/deps/vomp/vomp/modules/transformer/__init__.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .blocks import *
+from .modulated import *
diff --git a/deps/vomp/vomp/modules/transformer/blocks.py b/deps/vomp/vomp/modules/transformer/blocks.py
new file mode 100644
index 0000000000000000000000000000000000000000..272965d71efb81e927031af246bb177f08635aca
--- /dev/null
+++ b/deps/vomp/vomp/modules/transformer/blocks.py
@@ -0,0 +1,211 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from ..attention import MultiHeadAttention
+from ..norm import LayerNorm32
+
+
+class AbsolutePositionEmbedder(nn.Module):
+ """
+ Embeds spatial positions into vector representations.
+ """
+
+ def __init__(self, channels: int, in_channels: int = 3):
+ super().__init__()
+ self.channels = channels
+ self.in_channels = in_channels
+ self.freq_dim = channels // in_channels // 2
+ self.freqs = torch.arange(self.freq_dim, dtype=torch.float32) / self.freq_dim
+ self.freqs = 1.0 / (10000**self.freqs)
+
+ def _sin_cos_embedding(self, x: torch.Tensor) -> torch.Tensor:
+ """
+ Create sinusoidal position embeddings.
+
+ Args:
+ x: a 1-D Tensor of N indices
+
+ Returns:
+ an (N, D) Tensor of positional embeddings.
+ """
+ self.freqs = self.freqs.to(x.device)
+ out = torch.outer(x, self.freqs)
+ out = torch.cat([torch.sin(out), torch.cos(out)], dim=-1)
+ return out
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ """
+ Args:
+ x (torch.Tensor): (N, D) tensor of spatial positions
+ """
+ N, D = x.shape
+ assert (
+ D == self.in_channels
+ ), "Input dimension must match number of input channels"
+ embed = self._sin_cos_embedding(x.reshape(-1))
+ embed = embed.reshape(N, -1)
+ if embed.shape[1] < self.channels:
+ embed = torch.cat(
+ [
+ embed,
+ torch.zeros(N, self.channels - embed.shape[1], device=embed.device),
+ ],
+ dim=-1,
+ )
+ return embed
+
+
+class FeedForwardNet(nn.Module):
+ def __init__(self, channels: int, mlp_ratio: float = 4.0):
+ super().__init__()
+ self.mlp = nn.Sequential(
+ nn.Linear(channels, int(channels * mlp_ratio)),
+ nn.GELU(approximate="tanh"),
+ nn.Linear(int(channels * mlp_ratio), channels),
+ )
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return self.mlp(x)
+
+
+class TransformerBlock(nn.Module):
+ """
+ Transformer block (MSA + FFN).
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal["full", "windowed"] = "full",
+ window_size: Optional[int] = None,
+ shift_window: Optional[int] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qkv_bias: bool = True,
+ ln_affine: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.norm1 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.attn = MultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_window=shift_window,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.mlp = FeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+
+ def _forward(self, x: torch.Tensor) -> torch.Tensor:
+ h = self.norm1(x)
+ h = self.attn(h)
+ x = x + h
+ h = self.norm2(x)
+ h = self.mlp(h)
+ x = x + h
+ return x
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, use_reentrant=False
+ )
+ else:
+ return self._forward(x)
+
+
+class TransformerCrossBlock(nn.Module):
+ """
+ Transformer cross-attention block (MSA + MCA + FFN).
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ ctx_channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal["full", "windowed"] = "full",
+ window_size: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qk_rms_norm_cross: bool = False,
+ qkv_bias: bool = True,
+ ln_affine: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.norm1 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.norm3 = LayerNorm32(channels, elementwise_affine=ln_affine, eps=1e-6)
+ self.self_attn = MultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ type="self",
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_window=shift_window,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.cross_attn = MultiHeadAttention(
+ channels,
+ ctx_channels=ctx_channels,
+ num_heads=num_heads,
+ type="cross",
+ attn_mode="full",
+ qkv_bias=qkv_bias,
+ qk_rms_norm=qk_rms_norm_cross,
+ )
+ self.mlp = FeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+
+ def _forward(self, x: torch.Tensor, context: torch.Tensor):
+ h = self.norm1(x)
+ h = self.self_attn(h)
+ x = x + h
+ h = self.norm2(x)
+ h = self.cross_attn(h, context)
+ x = x + h
+ h = self.norm3(x)
+ h = self.mlp(h)
+ x = x + h
+ return x
+
+ def forward(self, x: torch.Tensor, context: torch.Tensor):
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, context, use_reentrant=False
+ )
+ else:
+ return self._forward(x, context)
diff --git a/deps/vomp/vomp/modules/transformer/modulated.py b/deps/vomp/vomp/modules/transformer/modulated.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c4e388a876e8a972d98571388217df6db46d779
--- /dev/null
+++ b/deps/vomp/vomp/modules/transformer/modulated.py
@@ -0,0 +1,183 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import torch.nn as nn
+from ..attention import MultiHeadAttention
+from ..norm import LayerNorm32
+from .blocks import FeedForwardNet
+
+
+class ModulatedTransformerBlock(nn.Module):
+ """
+ Transformer block (MSA + FFN) with adaptive layer norm conditioning.
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal["full", "windowed"] = "full",
+ window_size: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qkv_bias: bool = True,
+ share_mod: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.share_mod = share_mod
+ self.norm1 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.attn = MultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_window=shift_window,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.mlp = FeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+ if not share_mod:
+ self.adaLN_modulation = nn.Sequential(
+ nn.SiLU(), nn.Linear(channels, 6 * channels, bias=True)
+ )
+
+ def _forward(self, x: torch.Tensor, mod: torch.Tensor) -> torch.Tensor:
+ if self.share_mod:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(
+ 6, dim=1
+ )
+ else:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.adaLN_modulation(mod).chunk(6, dim=1)
+ )
+ h = self.norm1(x)
+ h = h * (1 + scale_msa.unsqueeze(1)) + shift_msa.unsqueeze(1)
+ h = self.attn(h)
+ h = h * gate_msa.unsqueeze(1)
+ x = x + h
+ h = self.norm2(x)
+ h = h * (1 + scale_mlp.unsqueeze(1)) + shift_mlp.unsqueeze(1)
+ h = self.mlp(h)
+ h = h * gate_mlp.unsqueeze(1)
+ x = x + h
+ return x
+
+ def forward(self, x: torch.Tensor, mod: torch.Tensor) -> torch.Tensor:
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, mod, use_reentrant=False
+ )
+ else:
+ return self._forward(x, mod)
+
+
+class ModulatedTransformerCrossBlock(nn.Module):
+ """
+ Transformer cross-attention block (MSA + MCA + FFN) with adaptive layer norm conditioning.
+ """
+
+ def __init__(
+ self,
+ channels: int,
+ ctx_channels: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ attn_mode: Literal["full", "windowed"] = "full",
+ window_size: Optional[int] = None,
+ shift_window: Optional[Tuple[int, int, int]] = None,
+ use_checkpoint: bool = False,
+ use_rope: bool = False,
+ qk_rms_norm: bool = False,
+ qk_rms_norm_cross: bool = False,
+ qkv_bias: bool = True,
+ share_mod: bool = False,
+ ):
+ super().__init__()
+ self.use_checkpoint = use_checkpoint
+ self.share_mod = share_mod
+ self.norm1 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.norm2 = LayerNorm32(channels, elementwise_affine=True, eps=1e-6)
+ self.norm3 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
+ self.self_attn = MultiHeadAttention(
+ channels,
+ num_heads=num_heads,
+ type="self",
+ attn_mode=attn_mode,
+ window_size=window_size,
+ shift_window=shift_window,
+ qkv_bias=qkv_bias,
+ use_rope=use_rope,
+ qk_rms_norm=qk_rms_norm,
+ )
+ self.cross_attn = MultiHeadAttention(
+ channels,
+ ctx_channels=ctx_channels,
+ num_heads=num_heads,
+ type="cross",
+ attn_mode="full",
+ qkv_bias=qkv_bias,
+ qk_rms_norm=qk_rms_norm_cross,
+ )
+ self.mlp = FeedForwardNet(
+ channels,
+ mlp_ratio=mlp_ratio,
+ )
+ if not share_mod:
+ self.adaLN_modulation = nn.Sequential(
+ nn.SiLU(), nn.Linear(channels, 6 * channels, bias=True)
+ )
+
+ def _forward(self, x: torch.Tensor, mod: torch.Tensor, context: torch.Tensor):
+ if self.share_mod:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(
+ 6, dim=1
+ )
+ else:
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.adaLN_modulation(mod).chunk(6, dim=1)
+ )
+ h = self.norm1(x)
+ h = h * (1 + scale_msa.unsqueeze(1)) + shift_msa.unsqueeze(1)
+ h = self.self_attn(h)
+ h = h * gate_msa.unsqueeze(1)
+ x = x + h
+ h = self.norm2(x)
+ h = self.cross_attn(h, context)
+ x = x + h
+ h = self.norm3(x)
+ h = h * (1 + scale_mlp.unsqueeze(1)) + shift_mlp.unsqueeze(1)
+ h = self.mlp(h)
+ h = h * gate_mlp.unsqueeze(1)
+ x = x + h
+ return x
+
+ def forward(self, x: torch.Tensor, mod: torch.Tensor, context: torch.Tensor):
+ if self.use_checkpoint:
+ return torch.utils.checkpoint.checkpoint(
+ self._forward, x, mod, context, use_reentrant=False
+ )
+ else:
+ return self._forward(x, mod, context)
diff --git a/deps/vomp/vomp/modules/utils.py b/deps/vomp/vomp/modules/utils.py
new file mode 100755
index 0000000000000000000000000000000000000000..67eb53b6f1b3509a7c98e0b144b0805c09e25e7c
--- /dev/null
+++ b/deps/vomp/vomp/modules/utils.py
@@ -0,0 +1,70 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch.nn as nn
+from ..modules import sparse as sp
+
+FP16_MODULES = (
+ nn.Conv1d,
+ nn.Conv2d,
+ nn.Conv3d,
+ nn.ConvTranspose1d,
+ nn.ConvTranspose2d,
+ nn.ConvTranspose3d,
+ nn.Linear,
+ sp.SparseConv3d,
+ sp.SparseInverseConv3d,
+ sp.SparseLinear,
+)
+
+
+def convert_module_to_f16(l):
+ """
+ Convert primitive modules to float16.
+ """
+ if isinstance(l, FP16_MODULES):
+ for p in l.parameters():
+ p.data = p.data.half()
+
+
+def convert_module_to_f32(l):
+ """
+ Convert primitive modules to float32, undoing convert_module_to_f16().
+ """
+ if isinstance(l, FP16_MODULES):
+ for p in l.parameters():
+ p.data = p.data.float()
+
+
+def zero_module(module):
+ """
+ Zero out the parameters of a module and return it.
+ """
+ for p in module.parameters():
+ p.detach().zero_()
+ return module
+
+
+def scale_module(module, scale):
+ """
+ Scale the parameters of a module and return it.
+ """
+ for p in module.parameters():
+ p.detach().mul_(scale)
+ return module
+
+
+def modulate(x, shift, scale):
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
diff --git a/deps/vomp/vomp/representations/__init__.py b/deps/vomp/vomp/representations/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..6b073482f691be0ecaf4ef24a4023b697de03aee
--- /dev/null
+++ b/deps/vomp/vomp/representations/__init__.py
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .gaussian import Gaussian
diff --git a/deps/vomp/vomp/representations/gaussian/__init__.py b/deps/vomp/vomp/representations/gaussian/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..507ec8d308f5d5c4d76b171ee91b6a4c3b9dd799
--- /dev/null
+++ b/deps/vomp/vomp/representations/gaussian/__init__.py
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .gaussian_model import Gaussian
diff --git a/deps/vomp/vomp/representations/gaussian/gaussian_model.py b/deps/vomp/vomp/representations/gaussian/gaussian_model.py
new file mode 100755
index 0000000000000000000000000000000000000000..f25920d256dbe5d2d8eb3a7ac3d0fc429213d58d
--- /dev/null
+++ b/deps/vomp/vomp/representations/gaussian/gaussian_model.py
@@ -0,0 +1,364 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import numpy as np
+from plyfile import PlyData, PlyElement
+from .general_utils import inverse_sigmoid, strip_symmetric, build_scaling_rotation
+import utils3d
+import kaolin
+
+
+def transform_shs(shs_feat, rotation_matrix):
+ try:
+ # Solution from: https://github.com/graphdeco-inria/gaussian-splatting/issues/176
+ from e3nn import o3
+
+ ## rotate shs
+ device = shs_feat.device
+ P = torch.tensor(
+ [[0, 0, 1], [1, 0, 0], [0, 1, 0]],
+ dtype=rotation_matrix.dtype,
+ device=rotation_matrix.device,
+ ) # switch axes: yzx -> xyz
+ permuted_rotation_matrix = torch.linalg.inv(P) @ rotation_matrix @ P
+ rot_angles = o3._rotation.matrix_to_angles(permuted_rotation_matrix.cpu())
+ # Construction coefficient
+ D_1 = o3.wigner_D(1, rot_angles[0], -rot_angles[1], rot_angles[2]).to(device)
+ D_2 = o3.wigner_D(2, rot_angles[0], -rot_angles[1], rot_angles[2]).to(device)
+ D_3 = o3.wigner_D(3, rot_angles[0], -rot_angles[1], rot_angles[2]).to(device)
+
+ # rotation of the shs features
+ res = torch.zeros_like(shs_feat)
+ res[:, :3] = D_1 @ shs_feat[:, :3]
+ res[:, 3:8] = D_2 @ shs_feat[:, 3:8]
+ res[:, 8:15] = D_3 @ shs_feat[:, 8:15]
+ return res
+ except Exception as e:
+ logger.error(f"Failed to transform sh features with error: {e}")
+ return shs_feat
+
+
+def transform_xyz(xyz: torch.Tensor, se_transform: torch.Tensor):
+ res = (
+ se_transform[None, :3, :3] @ xyz[:, :, None] + se_transform[None, :3, 3:]
+ ).squeeze(-1)
+ return res
+
+
+def transform_rot(rot: torch.Tensor, transform: torch.Tensor):
+ rot_quat = kaolin.math.quat.quat_from_rot33(transform[:3, :3].unsqueeze(0))
+ rot_unit = rot / torch.linalg.norm(rot, dim=-1).unsqueeze(-1)
+
+ # Note: gsplats use Hamiltonion convention [real, imag], whereas Kaolin uses the other convention[imag, real]
+ rot_unit = torch.cat([rot_unit[:, 1:], rot_unit[:, :1]], dim=-1)
+
+ result = kaolin.math.quat.quat_mul(rot_quat, rot_unit)
+ result = torch.cat([result[:, 3:], result[:, :3]], dim=-1)
+ return result
+
+
+def decompose_4x4_transform(transform):
+ """Decompose 4x4 transform into translation, rotation, scale.
+ Returns:
+ translation, rotation, scale
+ """
+ translation = transform[:3, 3:]
+ scale = torch.linalg.norm(transform[:3, :3], dim=0)
+ rotation = transform[:3, :3] / scale.unsqueeze(0)
+ return translation, rotation, scale
+
+
+def transform_gaussians(
+ xyz, rotations, raw_scales, transform, shs_feat=None, use_log_scales=True
+):
+ translation, rotation, scale = decompose_4x4_transform(transform)
+
+ new_xyz = transform_xyz(xyz, transform)
+ new_rotations = transform_rot(rotations, rotation)
+
+ if not use_log_scales:
+ new_scales = raw_scales * scale.unsqueeze(0)
+ else:
+ scaling_norm_factor = torch.log(scale).unsqueeze(0) / raw_scales + 1
+ new_scales = raw_scales * scaling_norm_factor
+
+ if shs_feat is None:
+ return new_xyz, new_rotations, new_scales
+ new_shs_feat = transform_shs(shs_feat, transform[:3, :3])
+ return new_xyz, new_rotations, new_scales, new_shs_feat
+
+
+class Gaussian:
+ def __init__(
+ self,
+ aabb: list,
+ sh_degree: int = 0,
+ mininum_kernel_size: float = 0.0,
+ scaling_bias: float = 0.01,
+ opacity_bias: float = 0.1,
+ scaling_activation: str = "exp",
+ device="cuda",
+ ):
+ self.init_params = {
+ "aabb": aabb,
+ "sh_degree": sh_degree,
+ "mininum_kernel_size": mininum_kernel_size,
+ "scaling_bias": scaling_bias,
+ "opacity_bias": opacity_bias,
+ "scaling_activation": scaling_activation,
+ }
+
+ self.sh_degree = sh_degree
+ self.active_sh_degree = sh_degree
+ self.mininum_kernel_size = mininum_kernel_size
+ self.scaling_bias = scaling_bias
+ self.opacity_bias = opacity_bias
+ self.scaling_activation_type = scaling_activation
+ self.device = device
+ self.aabb = torch.tensor(aabb, dtype=torch.float32, device=device)
+ self.setup_functions()
+
+ self._xyz = None
+ self._features_dc = None
+ self._features_rest = None
+ self._scaling = None
+ self._rotation = None
+ self._opacity = None
+
+ def setup_functions(self):
+ def build_covariance_from_scaling_rotation(scaling, scaling_modifier, rotation):
+ L = build_scaling_rotation(scaling_modifier * scaling, rotation)
+ actual_covariance = L @ L.transpose(1, 2)
+ symm = strip_symmetric(actual_covariance)
+ return symm
+
+ if self.scaling_activation_type == "exp":
+ self.scaling_activation = torch.exp
+ self.inverse_scaling_activation = torch.log
+ elif self.scaling_activation_type == "softplus":
+ self.scaling_activation = torch.nn.functional.softplus
+ self.inverse_scaling_activation = lambda x: x + torch.log(-torch.expm1(-x))
+
+ self.covariance_activation = build_covariance_from_scaling_rotation
+
+ self.opacity_activation = torch.sigmoid
+ self.inverse_opacity_activation = inverse_sigmoid
+
+ self.rotation_activation = torch.nn.functional.normalize
+
+ self.scale_bias = self.inverse_scaling_activation(
+ torch.tensor(self.scaling_bias)
+ ).cuda()
+ self.rots_bias = torch.zeros((4)).cuda()
+ self.rots_bias[0] = 1
+ self.opacity_bias = self.inverse_opacity_activation(
+ torch.tensor(self.opacity_bias)
+ ).cuda()
+
+ @property
+ def get_scaling(self):
+ scales = self.scaling_activation(self._scaling + self.scale_bias)
+ scales = torch.square(scales) + self.mininum_kernel_size**2
+ scales = torch.sqrt(scales)
+ return scales
+
+ @property
+ def get_rotation(self):
+ return self.rotation_activation(self._rotation + self.rots_bias[None, :])
+
+ @property
+ def get_xyz(self):
+ return self._xyz * self.aabb[None, 3:] + self.aabb[None, :3]
+
+ @property
+ def get_features(self):
+ return (
+ torch.cat((self._features_dc, self._features_rest), dim=1)
+ if self._features_rest is not None
+ else self._features_dc
+ )
+
+ @property
+ def get_opacity(self):
+ return self.opacity_activation(self._opacity + self.opacity_bias)
+
+ def get_covariance(self, scaling_modifier=1):
+ return self.covariance_activation(
+ self.get_scaling, scaling_modifier, self._rotation + self.rots_bias[None, :]
+ )
+
+ def from_scaling(self, scales):
+ scales = torch.sqrt(torch.square(scales) - self.mininum_kernel_size**2)
+ self._scaling = self.inverse_scaling_activation(scales) - self.scale_bias
+
+ def from_rotation(self, rots):
+ self._rotation = rots - self.rots_bias[None, :]
+
+ def from_xyz(self, xyz):
+ self._xyz = (xyz - self.aabb[None, :3]) / self.aabb[None, 3:]
+
+ def from_features(self, features):
+ self._features_dc = features
+
+ def from_opacity(self, opacities):
+ self._opacity = self.inverse_opacity_activation(opacities) - self.opacity_bias
+
+ def construct_list_of_attributes(self):
+ l = ["x", "y", "z", "nx", "ny", "nz"]
+ # All channels except the 3 DC
+ for i in range(self._features_dc.shape[1] * self._features_dc.shape[2]):
+ l.append("f_dc_{}".format(i))
+ l.append("opacity")
+ for i in range(self._scaling.shape[1]):
+ l.append("scale_{}".format(i))
+ for i in range(self._rotation.shape[1]):
+ l.append("rot_{}".format(i))
+ return l
+
+ def save_ply(self, path, transform=[[1, 0, 0], [0, 0, -1], [0, 1, 0]]):
+ xyz = self.get_xyz.detach().cpu().numpy()
+ normals = np.zeros_like(xyz)
+ f_dc = (
+ self._features_dc.detach()
+ .transpose(1, 2)
+ .flatten(start_dim=1)
+ .contiguous()
+ .cpu()
+ .numpy()
+ )
+ opacities = inverse_sigmoid(self.get_opacity).detach().cpu().numpy()
+ scale = torch.log(self.get_scaling).detach().cpu().numpy()
+ rotation = (self._rotation + self.rots_bias[None, :]).detach().cpu().numpy()
+
+ if transform is not None:
+ transform = np.array(transform)
+ xyz = np.matmul(xyz, transform.T)
+ rotation = utils3d.numpy.quaternion_to_matrix(rotation)
+ rotation = np.matmul(transform, rotation)
+ rotation = utils3d.numpy.matrix_to_quaternion(rotation)
+
+ dtype_full = [
+ (attribute, "f4") for attribute in self.construct_list_of_attributes()
+ ]
+
+ elements = np.empty(xyz.shape[0], dtype=dtype_full)
+ attributes = np.concatenate(
+ (xyz, normals, f_dc, opacities, scale, rotation), axis=1
+ )
+ elements[:] = list(map(tuple, attributes))
+ el = PlyElement.describe(elements, "vertex")
+ PlyData([el]).write(path)
+
+ def load_ply(self, path, transform=[[1, 0, 0], [0, 0, -1], [0, 1, 0]]):
+ plydata = PlyData.read(path)
+
+ xyz = np.stack(
+ (
+ np.asarray(plydata.elements[0]["x"]),
+ np.asarray(plydata.elements[0]["y"]),
+ np.asarray(plydata.elements[0]["z"]),
+ ),
+ axis=1,
+ )
+ opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis]
+
+ features_dc = np.zeros((xyz.shape[0], 3, 1))
+ features_dc[:, 0, 0] = np.asarray(plydata.elements[0]["f_dc_0"])
+ features_dc[:, 1, 0] = np.asarray(plydata.elements[0]["f_dc_1"])
+ features_dc[:, 2, 0] = np.asarray(plydata.elements[0]["f_dc_2"])
+
+ if self.sh_degree > 0:
+ extra_f_names = [
+ p.name
+ for p in plydata.elements[0].properties
+ if p.name.startswith("f_rest_")
+ ]
+ extra_f_names = sorted(extra_f_names, key=lambda x: int(x.split("_")[-1]))
+ assert len(extra_f_names) == 3 * (self.sh_degree + 1) ** 2 - 3
+ features_extra = np.zeros((xyz.shape[0], len(extra_f_names)))
+ for idx, attr_name in enumerate(extra_f_names):
+ features_extra[:, idx] = np.asarray(plydata.elements[0][attr_name])
+ # Reshape (P,F*SH_coeffs) to (P, F, SH_coeffs except DC)
+ features_extra = features_extra.reshape(
+ (features_extra.shape[0], 3, (self.sh_degree + 1) ** 2 - 1)
+ )
+
+ scale_names = [
+ p.name
+ for p in plydata.elements[0].properties
+ if p.name.startswith("scale_")
+ ]
+ scale_names = sorted(scale_names, key=lambda x: int(x.split("_")[-1]))
+ scales = np.zeros((xyz.shape[0], len(scale_names)))
+ for idx, attr_name in enumerate(scale_names):
+ scales[:, idx] = np.asarray(plydata.elements[0][attr_name])
+
+ rot_names = [
+ p.name for p in plydata.elements[0].properties if p.name.startswith("rot")
+ ]
+ rot_names = sorted(rot_names, key=lambda x: int(x.split("_")[-1]))
+ rots = np.zeros((xyz.shape[0], len(rot_names)))
+ for idx, attr_name in enumerate(rot_names):
+ rots[:, idx] = np.asarray(plydata.elements[0][attr_name])
+
+ if transform is not None:
+ _transform = torch.eye(4, dtype=torch.float)
+ _transform[:3, :3] = torch.tensor(transform, dtype=torch.float)
+ xyz = torch.tensor(xyz)
+ rots = torch.tensor(rots)
+ scales = torch.tensor(scales)
+ xyz, rots, scales = transform_gaussians(xyz, rots, scales, _transform)
+ # transform = np.array(transform).astype(float)
+ # xyz = np.matmul(xyz, transform)
+ # rots = transform_rot(torch.tensor(rots), torch.tensor(transform)).numpy()
+ # rots = utils3d.numpy.quaternion_to_matrix(rots)
+ # rots = np.matmul(rots, transform)
+ # rots = utils3d.numpy.matrix_to_quaternion(rots)
+
+ # convert to actual gaussian attributes
+ xyz = torch.tensor(xyz, dtype=torch.float, device=self.device)
+ features_dc = (
+ torch.tensor(features_dc, dtype=torch.float, device=self.device)
+ .transpose(1, 2)
+ .contiguous()
+ )
+ if self.sh_degree > 0:
+ features_extra = (
+ torch.tensor(features_extra, dtype=torch.float, device=self.device)
+ .transpose(1, 2)
+ .contiguous()
+ )
+ opacities = torch.sigmoid(
+ torch.tensor(opacities, dtype=torch.float, device=self.device)
+ )
+ scales = torch.exp(torch.tensor(scales, dtype=torch.float, device=self.device))
+ rots = torch.tensor(rots, dtype=torch.float, device=self.device)
+
+ # convert to _hidden attributes
+ self._xyz = (xyz - self.aabb[None, :3]) / self.aabb[None, 3:]
+ self._features_dc = features_dc
+ if self.sh_degree > 0:
+ self._features_rest = features_extra
+ else:
+ self._features_rest = None
+ self._opacity = self.inverse_opacity_activation(opacities) - self.opacity_bias
+ self._scaling = (
+ self.inverse_scaling_activation(
+ torch.sqrt(torch.square(scales) - self.mininum_kernel_size**2)
+ )
+ - self.scale_bias
+ )
+ self._rotation = rots - self.rots_bias[None, :]
diff --git a/deps/vomp/vomp/representations/gaussian/general_utils.py b/deps/vomp/vomp/representations/gaussian/general_utils.py
new file mode 100755
index 0000000000000000000000000000000000000000..bebb00c0044f1e1f9852730f09d60ea1e10a459f
--- /dev/null
+++ b/deps/vomp/vomp/representations/gaussian/general_utils.py
@@ -0,0 +1,166 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Copyright (C) 2023, Inria
+# GRAPHDECO research group, https://team.inria.fr/graphdeco
+# All rights reserved.
+#
+# This software is free for non-commercial, research and evaluation use
+# under the terms of the LICENSE.md file.
+#
+# For inquiries contact george.drettakis@inria.fr
+#
+
+import torch
+import sys
+from datetime import datetime
+import numpy as np
+import random
+
+
+def inverse_sigmoid(x):
+ return torch.log(x / (1 - x))
+
+
+def PILtoTorch(pil_image, resolution):
+ resized_image_PIL = pil_image.resize(resolution)
+ resized_image = torch.from_numpy(np.array(resized_image_PIL)) / 255.0
+ if len(resized_image.shape) == 3:
+ return resized_image.permute(2, 0, 1)
+ else:
+ return resized_image.unsqueeze(dim=-1).permute(2, 0, 1)
+
+
+def get_expon_lr_func(
+ lr_init, lr_final, lr_delay_steps=0, lr_delay_mult=1.0, max_steps=1000000
+):
+ """
+ Copied from Plenoxels
+
+ Continuous learning rate decay function. Adapted from JaxNeRF
+ The returned rate is lr_init when step=0 and lr_final when step=max_steps, and
+ is log-linearly interpolated elsewhere (equivalent to exponential decay).
+ If lr_delay_steps>0 then the learning rate will be scaled by some smooth
+ function of lr_delay_mult, such that the initial learning rate is
+ lr_init*lr_delay_mult at the beginning of optimization but will be eased back
+ to the normal learning rate when steps>lr_delay_steps.
+ :param conf: config subtree 'lr' or similar
+ :param max_steps: int, the number of steps during optimization.
+ :return HoF which takes step as input
+ """
+
+ def helper(step):
+ if step < 0 or (lr_init == 0.0 and lr_final == 0.0):
+ # Disable this parameter
+ return 0.0
+ if lr_delay_steps > 0:
+ # A kind of reverse cosine decay.
+ delay_rate = lr_delay_mult + (1 - lr_delay_mult) * np.sin(
+ 0.5 * np.pi * np.clip(step / lr_delay_steps, 0, 1)
+ )
+ else:
+ delay_rate = 1.0
+ t = np.clip(step / max_steps, 0, 1)
+ log_lerp = np.exp(np.log(lr_init) * (1 - t) + np.log(lr_final) * t)
+ return delay_rate * log_lerp
+
+ return helper
+
+
+def strip_lowerdiag(L):
+ uncertainty = torch.zeros((L.shape[0], 6), dtype=torch.float, device="cuda")
+
+ uncertainty[:, 0] = L[:, 0, 0]
+ uncertainty[:, 1] = L[:, 0, 1]
+ uncertainty[:, 2] = L[:, 0, 2]
+ uncertainty[:, 3] = L[:, 1, 1]
+ uncertainty[:, 4] = L[:, 1, 2]
+ uncertainty[:, 5] = L[:, 2, 2]
+ return uncertainty
+
+
+def strip_symmetric(sym):
+ return strip_lowerdiag(sym)
+
+
+def build_rotation(r):
+ norm = torch.sqrt(
+ r[:, 0] * r[:, 0] + r[:, 1] * r[:, 1] + r[:, 2] * r[:, 2] + r[:, 3] * r[:, 3]
+ )
+
+ q = r / norm[:, None]
+
+ R = torch.zeros((q.size(0), 3, 3), device="cuda")
+
+ r = q[:, 0]
+ x = q[:, 1]
+ y = q[:, 2]
+ z = q[:, 3]
+
+ R[:, 0, 0] = 1 - 2 * (y * y + z * z)
+ R[:, 0, 1] = 2 * (x * y - r * z)
+ R[:, 0, 2] = 2 * (x * z + r * y)
+ R[:, 1, 0] = 2 * (x * y + r * z)
+ R[:, 1, 1] = 1 - 2 * (x * x + z * z)
+ R[:, 1, 2] = 2 * (y * z - r * x)
+ R[:, 2, 0] = 2 * (x * z - r * y)
+ R[:, 2, 1] = 2 * (y * z + r * x)
+ R[:, 2, 2] = 1 - 2 * (x * x + y * y)
+ return R
+
+
+def build_scaling_rotation(s, r):
+ L = torch.zeros((s.shape[0], 3, 3), dtype=torch.float, device="cuda")
+ R = build_rotation(r)
+
+ L[:, 0, 0] = s[:, 0]
+ L[:, 1, 1] = s[:, 1]
+ L[:, 2, 2] = s[:, 2]
+
+ L = R @ L
+ return L
+
+
+def safe_state(silent):
+ old_f = sys.stdout
+
+ class F:
+ def __init__(self, silent):
+ self.silent = silent
+
+ def write(self, x):
+ if not self.silent:
+ if x.endswith("\n"):
+ old_f.write(
+ x.replace(
+ "\n",
+ " [{}]\n".format(
+ str(datetime.now().strftime("%d/%m %H:%M:%S"))
+ ),
+ )
+ )
+ else:
+ old_f.write(x)
+
+ def flush(self):
+ old_f.flush()
+
+ sys.stdout = F(silent)
+
+ random.seed(0)
+ np.random.seed(0)
+ torch.manual_seed(0)
+ torch.cuda.set_device(torch.device("cuda:0"))
diff --git a/deps/vomp/vomp/sim/__init__.py b/deps/vomp/vomp/sim/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/sim/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/sim/main.py b/deps/vomp/vomp/sim/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b5b620790745c6fb99f7aa6c7d8e626374da310
--- /dev/null
+++ b/deps/vomp/vomp/sim/main.py
@@ -0,0 +1,778 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import sys
+import os
+import pathlib
+import numpy as np
+import polyscope as ps
+from polyscope import imgui
+
+from uipc import view, Vector3, Transform, Logger, AngleAxis
+from uipc.core import Engine, World, Scene, SceneIO
+from uipc.gui import SceneGUI
+from uipc.unit import kPa, GPa
+from uipc.backend import SceneVisitor
+
+from vomp.sim.meshes import (
+ MeshProcessor,
+ load_visual_mesh,
+ write_visual_meshes_obj,
+ embed_visual_mesh_in_physics_tets,
+)
+
+
+def create_output_dir(output_path):
+ output_dir = pathlib.Path(output_path).absolute()
+ output_dir.mkdir(parents=True, exist_ok=True)
+ return str(output_dir)
+
+
+def setup_scene_from_config(config):
+ scene_config = Scene.default_config()
+
+ sim_params = config.get("simulation", {})
+ scene_config["dt"] = sim_params.get("dt", 0.02)
+
+ gravity = sim_params.get("gravity", [0.0, -9.8, 0.0])
+ scene_config["gravity"] = [[gravity[0]], [gravity[1]], [gravity[2]]]
+
+ contact_params = sim_params.get("contact", {})
+ scene_config["contact"]["friction"]["enable"] = contact_params.get(
+ "friction_enable", False
+ )
+ scene_config["contact"]["d_hat"] = contact_params.get("d_hat", 0.01)
+
+ return Scene(scene_config)
+
+
+def setup_contact_model(scene, config):
+ contact_config = config.get("contact_model", {})
+ friction = contact_config.get("friction", 0.5)
+ contact_resistance = contact_config.get("contact_resistance", 1.0) * GPa
+
+ scene.contact_tabular().default_model(friction, contact_resistance)
+
+
+def run_simulation(config_path):
+
+ with open(config_path, "r") as f:
+ config = json.load(f)
+
+ output_dir = create_output_dir(config["output"]["directory"])
+
+ log_level = config.get("logging", {}).get("level", "warn")
+ if log_level.lower() == "info":
+ Logger.set_level(Logger.Level.Info)
+ elif log_level.lower() == "debug":
+ Logger.set_level(Logger.Level.Debug)
+ else:
+ Logger.set_level(Logger.Level.Warn)
+
+ engine_type = config.get("engine", {}).get("type", "cuda")
+ engine = Engine(engine_type, output_dir)
+ world = World(engine)
+
+ scene = setup_scene_from_config(config)
+ setup_contact_model(scene, config)
+
+ mesh_processor = MeshProcessor(scene)
+
+ objects_config = config.get("objects", [])
+ all_meshes = mesh_processor.create_objects_from_config(objects_config)
+
+ ground_config = config.get("ground", {})
+ if ground_config.get("enable", True):
+ ground_height = ground_config.get("height", 0.0)
+ mesh_processor.create_ground(ground_height)
+
+ world.init(scene)
+
+ sio = SceneIO(scene)
+
+ save_meshes = config["output"].get("save_meshes", True)
+ obj_output_dir = None
+ if save_meshes:
+ obj_output_dir = os.path.join(output_dir, "surface_meshes")
+ os.makedirs(obj_output_dir, exist_ok=True)
+
+ show_gui = config.get("gui", {}).get("enable", True)
+
+ if show_gui:
+ run_with_gui(world, scene, sio, obj_output_dir, save_meshes, all_meshes, config)
+ else:
+ run_headless(world, scene, sio, obj_output_dir, save_meshes, config)
+
+
+def run_with_gui(world, scene, sio, obj_output_dir, save_meshes, all_meshes, config):
+
+ def compute_visual_vertices_from_barycentric(
+ physics_obj_index, embeddings, physics_positions
+ ):
+ """
+ Compute deformed visual vertex positions using barycentric coordinates.
+ This interpolates the visual mesh vertices based on the physics mesh deformation.
+ """
+ physics_mesh = all_meshes[physics_obj_index][0]
+ physics_tets = physics_mesh.tetrahedra()
+ tet_connectivity = view(physics_tets.topo())
+
+ deformed_visual_vertices = np.zeros((len(embeddings), 3), dtype=np.float32)
+
+ # Track warnings to avoid spamming console
+ warning_counts = {
+ "invalid_tet": 0,
+ "invalid_coords": 0,
+ "extreme_coords": 0,
+ "nan_inf": 0,
+ }
+
+ for j, (tet_idx, bary_coords) in enumerate(embeddings):
+ # Validate tetrahedron index
+ if tet_idx < 0 or tet_idx >= len(tet_connectivity):
+ warning_counts["invalid_tet"] += 1
+ continue
+
+ tet_verts = tet_connectivity[tet_idx]
+ tet_positions = physics_positions[tet_verts]
+
+ # Ensure correct shape
+ if tet_positions.shape != (4, 3):
+ tet_positions = tet_positions.reshape(4, 3)
+
+ if bary_coords.ndim > 1:
+ bary_coords = bary_coords.flatten()
+
+ # Validate barycentric coordinates
+ if len(bary_coords) != 4:
+ warning_counts["invalid_coords"] += 1
+ bary_coords = np.array([0.25, 0.25, 0.25, 0.25])
+
+ # Check for NaN or infinite values first
+ if np.any(np.isnan(bary_coords)) or np.any(np.isinf(bary_coords)):
+ warning_counts["nan_inf"] += 1
+ bary_coords = np.array([0.25, 0.25, 0.25, 0.25])
+ else:
+ # Check for extreme values and renormalize if needed
+ max_abs_coord = np.max(np.abs(bary_coords))
+ if max_abs_coord > 3.0:
+ warning_counts["extreme_coords"] += 1
+ # Scale down to reasonable range
+ bary_coords = bary_coords * (3.0 / max_abs_coord)
+
+ # Ensure sum is approximately 1.0
+ coord_sum = np.sum(bary_coords)
+ if abs(coord_sum) > 1e-10:
+ bary_coords = bary_coords / coord_sum
+ else:
+ bary_coords = np.array([0.25, 0.25, 0.25, 0.25])
+
+ # Compute interpolated position
+ deformed_pos = np.sum(bary_coords[:, np.newaxis] * tet_positions, axis=0)
+ deformed_visual_vertices[j] = deformed_pos
+
+ # Report warnings summary (only if there are warnings)
+ if any(warning_counts.values()):
+ print(
+ f"Barycentric interpolation warnings: {sum(warning_counts.values())} total issues"
+ )
+ if warning_counts["invalid_tet"] > 0:
+ print(
+ f" - {warning_counts['invalid_tet']} invalid tetrahedron indices"
+ )
+ if warning_counts["invalid_coords"] > 0:
+ print(
+ f" - {warning_counts['invalid_coords']} invalid coordinate lengths"
+ )
+ if warning_counts["extreme_coords"] > 0:
+ print(
+ f" - {warning_counts['extreme_coords']} extreme coordinates (renormalized)"
+ )
+ if warning_counts["nan_inf"] > 0:
+ print(f" - {warning_counts['nan_inf']} NaN/Inf coordinates")
+
+ return deformed_visual_vertices
+
+ sgui = SceneGUI(scene)
+
+ ps.init()
+ ground_height = config.get("ground", {}).get("height", 0.0)
+ ps.set_ground_plane_height(ground_height)
+
+ tri_surf, line_surf, point_surf = sgui.register()
+ if tri_surf:
+ tri_surf.set_edge_width(1)
+
+ visual_meshes = []
+ visual_mesh_objects = []
+ physics_initial_positions = []
+ physics_current_positions = []
+ visual_barycentric_embeddings = []
+ scene_object_names = []
+ current_visual_vertices = []
+
+ objects_config = config.get("objects", [])
+ for i, (physics_mesh, obj_config) in enumerate(
+ zip([m[0] for m in all_meshes], objects_config)
+ ):
+
+ physics_positions = physics_mesh.positions().view()
+ physics_pos_array = []
+ for j in range(len(physics_positions)):
+ pos = physics_positions[j]
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+ if isinstance(pos_list[0], list):
+ physics_pos_array.append(
+ [pos_list[0][0], pos_list[1][0], pos_list[2][0]]
+ )
+ else:
+ physics_pos_array.append(pos_list)
+ else:
+ physics_pos_array.append([float(pos[0]), float(pos[1]), float(pos[2])])
+
+ physics_pos_array = np.array(physics_pos_array, dtype=np.float32)
+ physics_initial_positions.append(physics_pos_array.copy())
+ physics_current_positions.append(physics_pos_array.copy())
+
+ obj_name = obj_config.get("name", f"object_{i+1}")
+ scene_object_names.append(obj_name)
+
+ has_visual_meshes = any(
+ obj_config.get("visual_mesh") for obj_config in objects_config
+ )
+
+ if has_visual_meshes:
+ print("Loading visual meshes from config...")
+
+ for physics_obj_index, (physics_mesh, obj_config) in enumerate(
+ zip([m[0] for m in all_meshes], objects_config)
+ ):
+ visual_mesh_path = obj_config.get("visual_mesh")
+
+ if not visual_mesh_path:
+ print(
+ f"Warning: No visual mesh specified for object {physics_obj_index+1} ({obj_config.get('name', 'unnamed')})"
+ )
+ continue
+
+ if not os.path.exists(visual_mesh_path):
+ print(
+ f"Warning: Visual mesh file {visual_mesh_path} not found for object {physics_obj_index+1}"
+ )
+ continue
+
+ print(
+ f"Loading visual mesh for object {physics_obj_index+1} from {visual_mesh_path}"
+ )
+
+ transform = Transform.Identity()
+ if "translation" in obj_config:
+ t = obj_config["translation"]
+ translation_vector = (
+ Vector3.UnitX() * t[0]
+ + Vector3.UnitY() * t[1]
+ + Vector3.UnitZ() * t[2]
+ )
+ transform.translate(translation_vector)
+ if "rotation" in obj_config:
+ r = obj_config["rotation"]
+ # Convert degrees to radians and apply rotations in order: Z, Y, X
+ import math
+
+ if r[2] != 0:
+ angle_rad = math.radians(r[2])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitZ()))
+ if r[1] != 0:
+ angle_rad = math.radians(r[1])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitY()))
+ if r[0] != 0:
+ angle_rad = math.radians(r[0])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitX()))
+
+ visual_scale = obj_config.get("scale", 1.0)
+ visual_transform = Transform.Identity()
+ visual_transform.scale(visual_scale)
+ visual_transform = transform * visual_transform
+
+ # Load visual mesh with the same normalization as specified in config
+ # This ensures visual mesh scale matches physics mesh scale
+ material = obj_config.get("material", {})
+ material_file = material.get("file", None)
+ normalize_visual = obj_config.get("normalize_visual_mesh", True)
+
+ # Apply the transform - normalization will happen inside if enabled
+ vertices, faces, uv_coords, face_uvs, face_materials, mtl_lib = (
+ load_visual_mesh(
+ visual_mesh_path,
+ 1.0,
+ visual_transform,
+ normalize_like_blender=normalize_visual,
+ material_file=material_file,
+ )
+ )
+
+ physics_pos_array = physics_initial_positions[physics_obj_index]
+
+ print(
+ f"Physics object {physics_obj_index+1}: {len(physics_pos_array)} physics vertices"
+ )
+
+ print(
+ f"Debug: Physics positions (world space) - X: [{np.min(physics_pos_array[:, 0]):.3f}, {np.max(physics_pos_array[:, 0]):.3f}], Y: [{np.min(physics_pos_array[:, 1]):.3f}, {np.max(physics_pos_array[:, 1]):.3f}], Z: [{np.min(physics_pos_array[:, 2]):.3f}, {np.max(physics_pos_array[:, 2]):.3f}]"
+ )
+ print(
+ f"Debug: Visual mesh (after transform) - X: [{np.min(vertices[:, 0]):.3f}, {np.max(vertices[:, 0]):.3f}], Y: [{np.min(vertices[:, 1]):.3f}, {np.max(vertices[:, 1]):.3f}], Z: [{np.min(vertices[:, 2]):.3f}, {np.max(vertices[:, 2]):.3f}]"
+ )
+
+ # Check alignment between visual and physics meshes
+ physics_centroid = np.mean(physics_pos_array, axis=0)
+ visual_centroid = np.mean(vertices, axis=0)
+ physics_size = np.max(physics_pos_array, axis=0) - np.min(
+ physics_pos_array, axis=0
+ )
+ visual_size = np.max(vertices, axis=0) - np.min(vertices, axis=0)
+
+ print(f"Debug: Physics centroid: {physics_centroid}, size: {physics_size}")
+ print(f"Debug: Visual centroid: {visual_centroid}, size: {visual_size}")
+
+ # Check if sizes match (within 10% - they should match if properly normalized/transformed)
+ size_ratio = np.linalg.norm(visual_size) / np.linalg.norm(physics_size)
+ print(f"Debug: Size ratio (visual/physics): {size_ratio:.3f}")
+
+ centroid_offset = physics_centroid - visual_centroid
+ offset_magnitude = np.linalg.norm(centroid_offset)
+ relative_offset = offset_magnitude / np.linalg.norm(physics_size)
+
+ # Decision logic for alignment:
+ # 1. If sizes match well (0.9 < ratio < 1.1), apply centroid alignment
+ # 2. If sizes don't match, user needs to fix the normalization/scale settings
+ if 0.9 < size_ratio < 1.1:
+ if relative_offset > 0.001: # More than 0.1% offset
+ print(
+ f"Sizes match, applying centroid alignment (offset: {centroid_offset}, relative: {relative_offset:.4f})"
+ )
+ aligned_visual_vertices = vertices + centroid_offset
+ else:
+ print(
+ f"Sizes and centroids match perfectly (offset: {relative_offset:.4f})"
+ )
+ aligned_visual_vertices = vertices
+ centroid_offset = np.array([0.0, 0.0, 0.0])
+ else:
+ print(
+ f"WARNING: Size mismatch detected! Visual and physics meshes have different scales."
+ )
+ print(
+ f" This usually means normalize_visual_mesh setting is incorrect."
+ )
+ print(
+ f" Physics size: {np.linalg.norm(physics_size):.3f}, Visual size: {np.linalg.norm(visual_size):.3f}"
+ )
+ print(
+ f" Attempting centroid alignment anyway, but results may be poor."
+ )
+ aligned_visual_vertices = vertices + centroid_offset
+
+ print(
+ f"Final visual range: X=[{np.min(aligned_visual_vertices[:, 0]):.3f}, {np.max(aligned_visual_vertices[:, 0]):.3f}], Y=[{np.min(aligned_visual_vertices[:, 1]):.3f}, {np.max(aligned_visual_vertices[:, 1]):.3f}], Z=[{np.min(aligned_visual_vertices[:, 2]):.3f}, {np.max(aligned_visual_vertices[:, 2]):.3f}]"
+ )
+
+ print(
+ f"Computing barycentric embedding for visual mesh {physics_obj_index+1}..."
+ )
+
+ # Get embedding optimization settings from config
+ embedding_config = config.get("embedding", {})
+ use_multiprocessing = embedding_config.get("use_multiprocessing", True)
+ n_processes = embedding_config.get("n_processes", None)
+ use_original_method = embedding_config.get("use_original_method", False)
+
+ embeddings, embedding_stats = embed_visual_mesh_in_physics_tets(
+ aligned_visual_vertices,
+ physics_mesh,
+ use_multiprocessing,
+ n_processes,
+ use_original_method,
+ )
+ visual_barycentric_embeddings.append(embeddings)
+
+ print(
+ f"Visual mesh {physics_obj_index+1} embedding: "
+ f"{embedding_stats['inside_count']} inside, "
+ f"{embedding_stats['outside_count']} outside, "
+ f"max distance = {embedding_stats['max_distance']:.4f}"
+ )
+
+ # Verify alignment after embedding
+ aligned_centroid = np.mean(aligned_visual_vertices, axis=0)
+ print(
+ f"Debug: Final aligned visual centroid: [{aligned_centroid[0]:.3f}, {aligned_centroid[1]:.3f}, {aligned_centroid[2]:.3f}]"
+ )
+
+ # Sanity check: compute distance between closest visual vertex and physics vertex
+ from scipy.spatial import cKDTree
+
+ physics_tree = cKDTree(physics_pos_array)
+ sample_visual_vertices = aligned_visual_vertices[
+ :: max(1, len(aligned_visual_vertices) // 100)
+ ] # Sample 100 points
+ sample_distances, _ = physics_tree.query(sample_visual_vertices)
+ avg_distance = np.mean(sample_distances)
+ max_distance = np.max(sample_distances)
+ print(
+ f"Debug: Visual to Physics mesh distances - Avg: {avg_distance:.4f}, Max: {max_distance:.4f}"
+ )
+
+ initial_visual_vertices = compute_visual_vertices_from_barycentric(
+ physics_obj_index, embeddings, physics_pos_array
+ )
+
+ visual_mesh_name = f"visual_mesh_{physics_obj_index+1}"
+ visual_mesh = ps.register_surface_mesh(
+ visual_mesh_name,
+ initial_visual_vertices,
+ faces,
+ edge_width=1.0,
+ color=(0.2, 0.8, 0.2),
+ )
+ visual_mesh.set_transparency(0.7)
+
+ # Extract material directory from the visual mesh path or material file
+ material_dir = None
+ if material_file:
+ material_dir = os.path.dirname(material_file)
+ elif visual_mesh_path:
+ material_dir = os.path.dirname(visual_mesh_path)
+
+ visual_meshes.append(
+ (
+ vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_config.get("name", f"object_{physics_obj_index+1}"),
+ face_materials,
+ mtl_lib,
+ )
+ )
+ visual_mesh_objects.append(visual_mesh)
+
+ current_visual_vertices.append(initial_visual_vertices.copy())
+
+ print(
+ f"Registered visual mesh {visual_mesh_name} with {len(vertices)} vertices (maps to physics object {physics_obj_index})"
+ )
+ else:
+ print("No visual meshes specified in config")
+
+ if save_meshes and visual_meshes:
+ initial_obj_path = os.path.join(
+ obj_output_dir, f"scene_surface_{world.frame():04d}.obj"
+ )
+ write_visual_meshes_obj(
+ initial_obj_path,
+ visual_meshes,
+ visual_mesh_objects,
+ current_visual_vertices,
+ )
+
+ run_simulation = config.get("simulation", {}).get("auto_start", False)
+ frame_count = 0
+ max_frames = config.get("simulation", {}).get("max_frames", 1000)
+
+ def update_physics_positions():
+ try:
+
+ scene_visitor = SceneVisitor(scene)
+ geo_slots = scene_visitor.geometries()
+
+ geometry_index = 0
+ for geo_slot in geo_slots:
+ if geometry_index >= len(physics_initial_positions):
+ break
+
+ geo = geo_slot.geometry()
+
+ if hasattr(geo, "positions"):
+
+ current_positions = geo.positions().view()
+
+ current_pos_array = []
+ for i in range(len(current_positions)):
+ pos = current_positions[i]
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+ if isinstance(pos_list[0], list):
+ current_pos_array.append(
+ [pos_list[0][0], pos_list[1][0], pos_list[2][0]]
+ )
+ else:
+ current_pos_array.append(pos_list)
+ else:
+ current_pos_array.append(
+ [float(pos[0]), float(pos[1]), float(pos[2])]
+ )
+
+ current_pos_array = np.array(current_pos_array, dtype=np.float32)
+
+ if frame_count % 20 == 0:
+ print(
+ f"Debug: Geometry {geometry_index+1}: Retrieved {len(current_pos_array)} vertices"
+ )
+
+ expected_vertex_count = len(
+ physics_initial_positions[geometry_index]
+ )
+ if len(current_pos_array) == expected_vertex_count:
+
+ displacement = (
+ current_pos_array
+ - physics_initial_positions[geometry_index]
+ )
+ displacement_magnitude = np.linalg.norm(displacement, axis=1)
+ max_displacement = np.max(displacement_magnitude)
+ avg_displacement = np.mean(displacement_magnitude)
+
+ if frame_count % 20 == 0:
+ print(
+ f"Debug: Physics object {geometry_index+1} - Max displacement: {max_displacement:.6f}, Avg displacement: {avg_displacement:.6f}"
+ )
+ print(
+ f"Debug: Current avg Y: {np.mean(current_pos_array[:, 1]):.4f}, Initial avg Y: {np.mean(physics_initial_positions[geometry_index][:, 1]):.4f}"
+ )
+
+ physics_current_positions[geometry_index] = (
+ current_pos_array.copy()
+ )
+ else:
+ if frame_count % 20 == 0:
+ print(
+ f"Debug: Vertex count mismatch for object {geometry_index+1} (expected {expected_vertex_count}, got {len(current_pos_array)})"
+ )
+
+ geometry_index += 1
+
+ except Exception as e:
+ if frame_count % 20 == 0:
+ print(
+ f"Debug: Error getting current positions from geometry slots: {e}"
+ )
+
+ def update_visual_meshes():
+ for i, (
+ vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_name,
+ face_materials,
+ mtl_lib,
+ ) in enumerate(visual_meshes):
+ if i < len(visual_barycentric_embeddings) and physics_obj_index < len(
+ physics_current_positions
+ ):
+
+ current_physics = physics_current_positions[physics_obj_index]
+ embeddings = visual_barycentric_embeddings[i]
+
+ deformed_visual_vertices = compute_visual_vertices_from_barycentric(
+ physics_obj_index, embeddings, current_physics
+ )
+
+ if frame_count % 20 == 0:
+ initial_visual_aligned = vertices + centroid_offset
+ displacement = deformed_visual_vertices - initial_visual_aligned
+ displacement_magnitude = np.linalg.norm(displacement, axis=1)
+ max_displacement = np.max(displacement_magnitude)
+ avg_displacement = np.mean(displacement_magnitude)
+
+ print(
+ f"Debug: Visual mesh {i+1} (physics obj {physics_obj_index+1}) - Max displacement: {max_displacement:.6f}, Avg displacement: {avg_displacement:.6f}"
+ )
+
+ visual_mesh_objects[i].update_vertex_positions(deformed_visual_vertices)
+
+ current_visual_vertices[i] = deformed_visual_vertices.copy()
+
+ def on_update():
+ nonlocal run_simulation, frame_count
+
+ if imgui.Button("Start/Stop Simulation"):
+ run_simulation = not run_simulation
+
+ imgui.SameLine()
+ if imgui.Button("Reset"):
+
+ mesh_processor = MeshProcessor(scene)
+ objects_config = config.get("objects", [])
+ for i, (mesh, obj_config) in enumerate(
+ zip([m[0] for m in all_meshes], objects_config)
+ ):
+ transform = Transform.Identity()
+ if "translation" in obj_config:
+ t = obj_config["translation"]
+
+ translation_vector = (
+ Vector3.UnitX() * t[0]
+ + Vector3.UnitY() * t[1]
+ + Vector3.UnitZ() * t[2]
+ )
+ transform.translate(translation_vector)
+ if "rotation" in obj_config:
+ r = obj_config["rotation"]
+ # Convert degrees to radians and apply rotations in order: Z, Y, X
+ import math
+
+ if r[2] != 0:
+ angle_rad = math.radians(r[2])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitZ()))
+ if r[1] != 0:
+ angle_rad = math.radians(r[1])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitY()))
+ if r[0] != 0:
+ angle_rad = math.radians(r[0])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitX()))
+ view(mesh.transforms())[0] = transform.matrix()
+
+ for i, (
+ vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_name,
+ face_materials,
+ mtl_lib,
+ ) in enumerate(visual_meshes):
+ if i < len(visual_barycentric_embeddings) and physics_obj_index < len(
+ physics_initial_positions
+ ):
+
+ initial_physics = physics_initial_positions[physics_obj_index]
+ embeddings = visual_barycentric_embeddings[i]
+
+ reset_visual_vertices = compute_visual_vertices_from_barycentric(
+ physics_obj_index, embeddings, initial_physics
+ )
+
+ visual_mesh_objects[i].update_vertex_positions(
+ reset_visual_vertices
+ )
+
+ current_visual_vertices[i] = reset_visual_vertices.copy()
+
+ for i in range(len(physics_current_positions)):
+ physics_current_positions[i] = physics_initial_positions[i].copy()
+
+ world.init(scene)
+ frame_count = 0
+
+ if save_meshes:
+ reset_obj_path = os.path.join(
+ obj_output_dir, f"scene_surface_{world.frame():04d}.obj"
+ )
+
+ if visual_meshes:
+ write_visual_meshes_obj(
+ reset_obj_path,
+ visual_meshes,
+ visual_mesh_objects,
+ current_visual_vertices,
+ )
+ else:
+ sio.write_surface(reset_obj_path)
+
+ imgui.Text(f"Frame: {frame_count}")
+ imgui.Text(f"Max Frames: {max_frames}")
+ imgui.Text(f"Time: {frame_count * config['simulation'].get('dt', 0.02):.2f}s")
+ imgui.Text(f"Objects: {len(all_meshes)}")
+ imgui.Text(f"World Frame: {world.frame()}")
+ imgui.Text(f"Visual Meshes: {len(visual_meshes)}")
+
+ if run_simulation and frame_count < max_frames:
+ world.advance()
+ world.retrieve()
+ sgui.update()
+
+ update_physics_positions()
+
+ update_visual_meshes()
+
+ frame_count += 1
+
+ if save_meshes:
+ step_obj_path = os.path.join(
+ obj_output_dir, f"scene_surface_{world.frame():04d}.obj"
+ )
+
+ if visual_meshes:
+ write_visual_meshes_obj(
+ step_obj_path,
+ visual_meshes,
+ visual_mesh_objects,
+ current_visual_vertices,
+ )
+ else:
+ sio.write_surface(step_obj_path)
+
+ if frame_count >= max_frames:
+ run_simulation = False
+
+ ps.set_user_callback(on_update)
+ ps.show()
+
+
+def run_headless(world, scene, sio, obj_output_dir, save_meshes, config):
+ max_frames = config.get("simulation", {}).get("max_frames", 100)
+
+ for frame in range(max_frames):
+ world.advance()
+ world.retrieve()
+
+ if save_meshes:
+ step_obj_path = os.path.join(
+ obj_output_dir, f"scene_surface_{world.frame():04d}.obj"
+ )
+
+ sio.write_surface(step_obj_path)
+
+ if frame % 10 == 0:
+ print(f"Frame {frame}/{max_frames}")
+
+ print(f"Simulation completed: {max_frames} frames")
+
+
+def main():
+ if len(sys.argv) < 2:
+ print("Usage: python main.py ")
+ print(" config.json: JSON configuration file for the simulation")
+ return
+
+ config_path = sys.argv[1]
+ run_simulation(config_path)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/deps/vomp/vomp/sim/meshes.py b/deps/vomp/vomp/sim/meshes.py
new file mode 100644
index 0000000000000000000000000000000000000000..7151e1ce245f89558bf9031194a3680681c837c1
--- /dev/null
+++ b/deps/vomp/vomp/sim/meshes.py
@@ -0,0 +1,1912 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+import os
+import pathlib
+from multiprocessing import Pool, cpu_count
+from uipc import view, Vector3, Transform, AngleAxis, builtin
+from uipc.geometry import (
+ SimplicialComplexIO,
+ label_surface,
+ ground,
+ tetrahedralize,
+ tetmesh,
+)
+from uipc.constitution import StableNeoHookean, ElasticModuli
+from uipc.unit import kPa, GPa
+from scipy.spatial import cKDTree
+
+
+def compute_barycentric_coordinates(point, tet_vertices):
+ """
+ Compute barycentric coordinates of a point with respect to a tetrahedron.
+ Returns coordinates [lambda1, lambda2, lambda3, lambda4] where the point = sum(lambda_i * v_i)
+ For points inside the tetrahedron, all lambdas are in [0, 1] and sum to 1.
+ For points outside, some lambdas may be negative or >1, but they still sum to 1.
+ """
+ v1, v2, v3, v4 = tet_vertices
+
+ # Set up linear system: point - v4 = A * [lambda1, lambda2, lambda3]^T
+ # where A = [v1-v4, v2-v4, v3-v4]
+ A = np.column_stack([v1 - v4, v2 - v4, v3 - v4])
+ b = point - v4
+
+ try:
+ det_A = np.linalg.det(A)
+
+ # Check for degenerate tetrahedra (volume too small)
+ # Volume of tetrahedron = |det(A)| / 6
+ if abs(det_A) < 1e-15:
+ # Degenerate tetrahedron - return centroid coordinates
+ return np.array([0.25, 0.25, 0.25, 0.25])
+
+ # Check condition number to avoid numerical instability
+ cond_A = np.linalg.cond(A)
+ if cond_A > 1e10:
+ # Poorly conditioned - return centroid coordinates
+ return np.array([0.25, 0.25, 0.25, 0.25])
+
+ # Solve for barycentric coordinates
+ lambdas_123 = np.linalg.solve(A, b)
+ lambda4 = 1.0 - np.sum(lambdas_123)
+
+ coords = np.array([lambdas_123[0], lambdas_123[1], lambdas_123[2], lambda4])
+
+ # Ensure coordinates are finite
+ if not np.all(np.isfinite(coords)):
+ return np.array([0.25, 0.25, 0.25, 0.25])
+
+ # For points significantly outside the tetrahedron, clamp to reasonable range
+ # This prevents extreme extrapolation that can cause visual artifacts
+ max_abs_coord = np.max(np.abs(coords))
+ if max_abs_coord > 3.0:
+ # Scale down to reasonable range while maintaining relative proportions
+ coords = coords * (3.0 / max_abs_coord)
+ # Renormalize to ensure sum = 1
+ coords = coords / np.sum(coords)
+
+ return coords
+
+ except np.linalg.LinAlgError:
+ # Singular matrix or other linear algebra error
+ return np.array([0.25, 0.25, 0.25, 0.25])
+
+
+def point_in_tetrahedron(point, tet_vertices, tolerance=1e-8):
+ """
+ Check if a point is inside a tetrahedron using barycentric coordinates.
+ A point is inside if all barycentric coordinates are non-negative (within tolerance).
+
+ Args:
+ point: 3D point coordinates
+ tet_vertices: Array of 4 vertices defining the tetrahedron
+ tolerance: Small negative value allowed for numerical precision (default 1e-8)
+
+ Returns:
+ (is_inside, bary_coords): Boolean indicating if inside, and the barycentric coordinates
+ """
+ bary_coords = compute_barycentric_coordinates(point, tet_vertices)
+
+ # Point is inside if all coordinates are >= -tolerance
+ # We use a small tolerance to account for numerical precision
+ is_inside = np.all(bary_coords >= -tolerance) and np.all(
+ bary_coords <= 1.0 + tolerance
+ )
+
+ return is_inside, bary_coords
+
+
+def _process_vertex_batch(args):
+ """
+ Worker function for multiprocessing
+ """
+ vertices_batch, tet_vertices_list, tet_centroids, start_idx = args
+
+ # Build KDTree for this worker (each process needs its own)
+ tet_tree = cKDTree(tet_centroids)
+
+ batch_embeddings = []
+ batch_stats = {"inside_count": 0, "outside_count": 0, "distances": []}
+
+ for i, visual_vertex in enumerate(vertices_batch):
+ tet_idx, bary_coords, distance = find_containing_tetrahedron_fast_optimized(
+ visual_vertex,
+ tet_vertices_list,
+ tet_tree,
+ fallback_closest=True,
+ max_check=500,
+ )
+
+ if tet_idx >= 0:
+ batch_embeddings.append((tet_idx, bary_coords))
+
+ if distance == 0.0:
+ batch_stats["inside_count"] += 1
+ else:
+ batch_stats["outside_count"] += 1
+ batch_stats["distances"].append(distance)
+ else:
+ # Fallback to closest centroid method
+ centroid_distances = np.linalg.norm(tet_centroids - visual_vertex, axis=1)
+ closest_tet_idx = np.argmin(centroid_distances)
+
+ bary_coords = compute_barycentric_coordinates(
+ visual_vertex, tet_vertices_list[closest_tet_idx]
+ )
+ batch_embeddings.append((closest_tet_idx, bary_coords))
+
+ batch_stats["outside_count"] += 1
+ batch_stats["distances"].append(centroid_distances[closest_tet_idx])
+
+ return start_idx, batch_embeddings, batch_stats
+
+
+def find_containing_tetrahedron_fast_optimized(
+ point, tet_vertices_list, tet_tree, fallback_closest=True, max_check=500
+):
+ """
+ Optimized version using KDTree for fast spatial queries
+ """
+ # Query the KDTree for nearest tetrahedron centroids
+ distances, indices = tet_tree.query(point, k=min(max_check, len(tet_vertices_list)))
+
+ # Handle single point case
+ if np.isscalar(distances):
+ distances = [distances]
+ indices = [indices]
+
+ best_tet_idx = -1
+ best_bary_coords = None
+ min_negative_weight = float("inf")
+
+ for dist, tet_idx in zip(distances, indices):
+ tet_vertices = tet_vertices_list[tet_idx]
+ is_inside, bary_coords = point_in_tetrahedron(point, tet_vertices)
+
+ if is_inside:
+ return tet_idx, bary_coords, 0.0
+
+ if fallback_closest:
+ max_negative = np.min(bary_coords)
+ if max_negative > min_negative_weight:
+ min_negative_weight = max_negative
+ best_tet_idx = tet_idx
+ best_bary_coords = bary_coords
+
+ if fallback_closest and best_tet_idx >= 0:
+ return best_tet_idx, best_bary_coords, abs(min_negative_weight)
+
+ return -1, None, float("inf")
+
+
+def find_containing_tetrahedron_fast(
+ point, tet_vertices_list, tet_centroids, fallback_closest=True, max_check=500
+):
+
+ distances_to_centroids = np.array(
+ [np.linalg.norm(point - centroid) for centroid in tet_centroids]
+ )
+ sorted_indices = np.argsort(distances_to_centroids)
+
+ best_tet_idx = -1
+ best_bary_coords = None
+ min_negative_weight = float("inf")
+
+ num_to_check = min(max_check, len(sorted_indices))
+
+ for i in range(num_to_check):
+ tet_idx = sorted_indices[i]
+ tet_vertices = tet_vertices_list[tet_idx]
+ is_inside, bary_coords = point_in_tetrahedron(point, tet_vertices)
+
+ if is_inside:
+
+ return tet_idx, bary_coords, 0.0
+
+ if fallback_closest:
+
+ max_negative = np.min(bary_coords)
+ if max_negative > min_negative_weight:
+ min_negative_weight = max_negative
+ best_tet_idx = tet_idx
+ best_bary_coords = bary_coords
+
+ if fallback_closest and best_tet_idx >= 0:
+ return best_tet_idx, best_bary_coords, abs(min_negative_weight)
+
+ return -1, None, float("inf")
+
+
+def find_containing_tetrahedron(point, tet_vertices_list, fallback_closest=True):
+ best_tet_idx = -1
+ best_bary_coords = None
+ min_negative_weight = float("inf")
+
+ for tet_idx, tet_vertices in enumerate(tet_vertices_list):
+ is_inside, bary_coords = point_in_tetrahedron(point, tet_vertices)
+
+ if is_inside:
+
+ return tet_idx, bary_coords, 0.0
+
+ if fallback_closest:
+
+ max_negative = np.min(bary_coords)
+ if max_negative > min_negative_weight:
+ min_negative_weight = max_negative
+ best_tet_idx = tet_idx
+ best_bary_coords = bary_coords
+
+ if fallback_closest and best_tet_idx >= 0:
+ return best_tet_idx, best_bary_coords, abs(min_negative_weight)
+
+ return -1, None, float("inf")
+
+
+def embed_visual_mesh_in_physics_tets(
+ visual_vertices,
+ physics_mesh,
+ use_multiprocessing=True,
+ n_processes=None,
+ use_original_method=False,
+):
+ print(f"Embedding {len(visual_vertices)} visual vertices in physics tetrahedra...")
+
+ physics_positions = physics_mesh.positions().view()
+ physics_tets = physics_mesh.tetrahedra()
+ tet_connectivity = view(physics_tets.topo())
+
+ physics_pos_array = []
+ for i in range(len(physics_positions)):
+ pos = physics_positions[i]
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+ if isinstance(pos_list[0], list):
+ physics_pos_array.append(
+ [pos_list[0][0], pos_list[1][0], pos_list[2][0]]
+ )
+ else:
+ physics_pos_array.append(pos_list)
+ else:
+ physics_pos_array.append([float(pos[0]), float(pos[1]), float(pos[2])])
+
+ physics_pos_array = np.array(physics_pos_array, dtype=np.float32)
+
+ tet_vertices_list = []
+ tet_centroids = []
+
+ for i in range(physics_tets.size()):
+ tet_verts = tet_connectivity[i]
+
+ tet_positions = physics_pos_array[tet_verts]
+
+ if tet_positions.shape != (4, 3):
+ tet_positions = tet_positions.reshape(4, 3)
+
+ tet_vertices_list.append(tet_positions)
+
+ centroid = np.mean(tet_positions, axis=0)
+ tet_centroids.append(centroid)
+
+ print(
+ f"Physics mesh has {len(physics_pos_array)} vertices and {len(tet_vertices_list)} tetrahedra"
+ )
+
+ tet_centroids = np.array(tet_centroids)
+
+ embeddings = []
+ stats = {
+ "inside_count": 0,
+ "outside_count": 0,
+ "max_distance": 0.0,
+ "avg_distance": 0.0,
+ }
+ distances = []
+
+ if use_original_method:
+ # Use the exact original method for comparison
+ print("Using original method for maximum accuracy...")
+ for i, visual_vertex in enumerate(visual_vertices):
+ tet_idx, bary_coords, distance = find_containing_tetrahedron_fast(
+ visual_vertex, tet_vertices_list, tet_centroids, fallback_closest=True
+ )
+
+ if tet_idx >= 0:
+ embeddings.append((tet_idx, bary_coords))
+
+ if distance == 0.0:
+ stats["inside_count"] += 1
+ else:
+ stats["outside_count"] += 1
+ distances.append(distance)
+ else:
+ # Fallback to closest centroid method
+ centroid_distances = [
+ np.linalg.norm(visual_vertex - centroid)
+ for centroid in tet_centroids
+ ]
+ closest_tet_idx = np.argmin(centroid_distances)
+
+ bary_coords = compute_barycentric_coordinates(
+ visual_vertex, tet_vertices_list[closest_tet_idx]
+ )
+ embeddings.append((closest_tet_idx, bary_coords))
+
+ stats["outside_count"] += 1
+ distances.append(centroid_distances[closest_tet_idx])
+
+ if (i + 1) % 1000 == 0:
+ print(f"Embedded {i + 1}/{len(visual_vertices)} vertices...")
+
+ elif use_multiprocessing and len(visual_vertices) > 1000:
+ # Use multiprocessing for large datasets
+ if n_processes is None:
+ n_processes = min(cpu_count(), 16) # Cap at 8 to avoid memory issues
+
+ print(f"Using multiprocessing with {n_processes} processes...")
+
+ # Split vertices into batches for each process
+ batch_size = max(
+ 100, len(visual_vertices) // (n_processes * 4)
+ ) # 4 batches per process
+ batches = []
+
+ for i in range(0, len(visual_vertices), batch_size):
+ end_idx = min(i + batch_size, len(visual_vertices))
+ batch = visual_vertices[i:end_idx]
+ batches.append((batch, tet_vertices_list, tet_centroids, i))
+
+ print(
+ f"Processing {len(visual_vertices)} vertices in {len(batches)} batches..."
+ )
+
+ # Process batches in parallel
+ with Pool(n_processes) as pool:
+ results = pool.map(_process_vertex_batch, batches)
+
+ # Combine results
+ embeddings = [None] * len(visual_vertices)
+ for start_idx, batch_embeddings, batch_stats in results:
+ for i, embedding in enumerate(batch_embeddings):
+ embeddings[start_idx + i] = embedding
+
+ stats["inside_count"] += batch_stats["inside_count"]
+ stats["outside_count"] += batch_stats["outside_count"]
+ distances.extend(batch_stats["distances"])
+
+ print("Multiprocessing completed!")
+
+ else:
+ # Use single-threaded processing for smaller datasets or when disabled
+ print("Building spatial index for fast tetrahedron lookup...")
+ tet_tree = cKDTree(tet_centroids)
+ print("Spatial index built successfully!")
+
+ # Process in batches for better memory management
+ batch_size = 1000
+ print(
+ f"Processing {len(visual_vertices)} vertices in batches of {batch_size}..."
+ )
+
+ for batch_start in range(0, len(visual_vertices), batch_size):
+ batch_end = min(batch_start + batch_size, len(visual_vertices))
+ batch_vertices = visual_vertices[batch_start:batch_end]
+
+ for i, visual_vertex in enumerate(batch_vertices):
+ global_i = batch_start + i
+
+ tet_idx, bary_coords, distance = (
+ find_containing_tetrahedron_fast_optimized(
+ visual_vertex,
+ tet_vertices_list,
+ tet_tree,
+ fallback_closest=True,
+ max_check=500,
+ )
+ )
+
+ if tet_idx >= 0:
+ embeddings.append((tet_idx, bary_coords))
+
+ if distance == 0.0:
+ stats["inside_count"] += 1
+ else:
+ stats["outside_count"] += 1
+ distances.append(distance)
+ else:
+ # Fallback to closest centroid method
+ centroid_distances = np.linalg.norm(
+ tet_centroids - visual_vertex, axis=1
+ )
+ closest_tet_idx = np.argmin(centroid_distances)
+
+ bary_coords = compute_barycentric_coordinates(
+ visual_vertex, tet_vertices_list[closest_tet_idx]
+ )
+ embeddings.append((closest_tet_idx, bary_coords))
+
+ stats["outside_count"] += 1
+ distances.append(centroid_distances[closest_tet_idx])
+
+ if (global_i + 1) % 1000 == 0:
+ print(f"Embedded {global_i + 1}/{len(visual_vertices)} vertices...")
+
+ if distances:
+ stats["max_distance"] = np.max(distances)
+ stats["avg_distance"] = np.mean(distances)
+
+ print(f"Embedding completed:")
+ print(f" Inside tetrahedra: {stats['inside_count']}")
+ print(f" Outside tetrahedra: {stats['outside_count']}")
+ print(f" Max distance to boundary: {stats['max_distance']:.6f}")
+ print(f" Average distance to boundary: {stats['avg_distance']:.6f}")
+
+ return embeddings, stats
+
+
+def load_visual_mesh(
+ file_path,
+ scale=1.0,
+ pre_transform=None,
+ normalize_like_blender=True,
+ material_file=None,
+):
+ if pre_transform is None:
+ pre_transform = Transform.Identity()
+ pre_transform.scale(scale)
+
+ # Extract scale from pre_transform to apply after normalization
+ if pre_transform is not None:
+ # Extract scale component from transform matrix more robustly
+ transform_matrix = pre_transform.matrix()
+ # Calculate scale as the length of the first column vector (assumes uniform scaling)
+ scale_vector = np.array(
+ [transform_matrix[0][0], transform_matrix[1][0], transform_matrix[2][0]]
+ )
+ transform_scale = float(np.linalg.norm(scale_vector))
+ else:
+ transform_scale = scale
+
+ if file_path.lower().endswith(".obj") and normalize_like_blender:
+ print(f"Loading OBJ with normalization: {file_path}")
+
+ # Load OBJ manually to preserve vertex-UV mapping and material assignments
+ vertices = []
+ uv_coords = []
+ faces = []
+ face_uvs = [] # UV indices for each face vertex
+ face_materials = [] # Material name for each face
+ current_material = None
+ mtl_lib = None
+
+ with open(file_path, "r") as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("mtllib "):
+ # Material library reference
+ mtl_lib = line.split()[1]
+ elif line.startswith("usemtl "):
+ # Material usage - affects subsequent faces
+ current_material = line.split()[1]
+ elif line.startswith("v "):
+ # Vertex: v x y z
+ parts = line.split()
+ vertices.append([float(parts[1]), float(parts[2]), float(parts[3])])
+ elif line.startswith("vt "):
+ # UV coordinate: vt u v [w]
+ parts = line.split()
+ uv_coords.append(
+ [float(parts[1]), float(parts[2])]
+ ) # Only use u,v (ignore w)
+ elif line.startswith("f "):
+ # Face: f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 [v4/vt4/vn4]
+ parts = line.split()[1:] # Skip 'f'
+ face_vertices = []
+ face_uv_indices = []
+
+ for part in parts:
+ indices = part.split("/")
+ # OBJ indices are 1-based, convert to 0-based
+ vertex_idx = int(indices[0]) - 1
+ uv_idx = (
+ int(indices[1]) - 1
+ if len(indices) > 1 and indices[1]
+ else vertex_idx
+ )
+
+ face_vertices.append(vertex_idx)
+ face_uv_indices.append(uv_idx)
+
+ # Convert quad faces to triangles if needed
+ if len(face_vertices) == 3:
+ faces.append(face_vertices)
+ face_uvs.append(face_uv_indices)
+ face_materials.append(current_material)
+ elif len(face_vertices) == 4:
+ # Split quad into two triangles
+ faces.append(
+ [face_vertices[0], face_vertices[1], face_vertices[2]]
+ )
+ faces.append(
+ [face_vertices[0], face_vertices[2], face_vertices[3]]
+ )
+ face_uvs.append(
+ [face_uv_indices[0], face_uv_indices[1], face_uv_indices[2]]
+ )
+ face_uvs.append(
+ [face_uv_indices[0], face_uv_indices[2], face_uv_indices[3]]
+ )
+ face_materials.append(current_material)
+ face_materials.append(current_material)
+
+ vertices = np.array(vertices, dtype=np.float32)
+ faces = np.array(faces, dtype=np.int32)
+ uv_coords = np.array(uv_coords, dtype=np.float32) if uv_coords else None
+
+ print(
+ f"Loaded OBJ: {len(vertices)} vertices, {len(faces)} faces, {len(uv_coords) if uv_coords is not None else 0} UVs"
+ )
+ if mtl_lib:
+ print(f"Material library: {mtl_lib}")
+
+ # Count materials used
+ unique_materials = set(mat for mat in face_materials if mat is not None)
+ if unique_materials:
+ print(f"Materials used: {', '.join(unique_materials)}")
+
+ # NEW APPROACH: Normalize then transform (instead of normalize with embedded scale)
+ # This matches how SimplicalComplexIO works
+ print(f"Applying normalization to match voxel coordinate space")
+
+ # Step 1: Compute original bbox
+ bbox_min = np.min(vertices, axis=0)
+ bbox_max = np.max(vertices, axis=0)
+ bbox_center = (bbox_min + bbox_max) / 2.0
+ bbox_size = bbox_max - bbox_min
+
+ print(f"Original bounding box: center={bbox_center}, size={bbox_size}")
+
+ # Step 2: Normalize to unit size centered at origin
+ max_dimension = np.max(bbox_size)
+ if max_dimension > 0:
+ # First center at origin
+ vertices = vertices - bbox_center
+ # Then scale to unit size
+ vertices = vertices / max_dimension
+ print(f"Normalized to unit size (scale={1.0/max_dimension:.6f})")
+
+ # Step 3: Now apply the full pre_transform (just like SimplicalComplexIO does)
+ if pre_transform is not None:
+ transform_matrix = np.array(pre_transform.matrix())
+ vertices_homogeneous = np.column_stack([vertices, np.ones(len(vertices))])
+ vertices_transformed = (transform_matrix @ vertices_homogeneous.T).T
+ vertices = vertices_transformed[:, :3].astype(np.float32)
+ print(f"Applied full pre_transform (scale={transform_scale:.6f})")
+
+ final_bbox_min = np.min(vertices, axis=0)
+ final_bbox_max = np.max(vertices, axis=0)
+ final_center = (final_bbox_min + final_bbox_max) / 2.0
+ final_size = final_bbox_max - final_bbox_min
+ print(f"Final bounding box: center={final_center}, size={final_size}")
+
+ import tempfile
+ import shutil
+
+ temp_dir = tempfile.mkdtemp(prefix="trellis_visual_mesh_")
+ temp_obj_path = os.path.join(temp_dir, "normalized_mesh.obj")
+
+ original_dir = os.path.dirname(file_path)
+ original_name = os.path.splitext(os.path.basename(file_path))[0]
+
+ mtl_path = os.path.join(original_dir, f"{original_name}.mtl")
+ if not os.path.exists(mtl_path):
+
+ mtl_path = os.path.join(original_dir, "new_mesh.mtl")
+
+ if os.path.exists(mtl_path):
+
+ temp_mtl_path = os.path.join(temp_dir, "normalized_mesh.mtl")
+ shutil.copy2(mtl_path, temp_mtl_path)
+
+ for file in os.listdir(original_dir):
+ if file.lower().endswith(
+ (".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp")
+ ):
+ shutil.copy2(os.path.join(original_dir, file), temp_dir)
+ print(f"Copied texture: {file}")
+
+ with open(temp_obj_path, "w") as f:
+ if os.path.exists(mtl_path):
+ f.write("mtllib normalized_mesh.mtl\n\n")
+
+ for v in vertices:
+ f.write(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}\n")
+ f.write("\n")
+
+ if uv_coords is not None:
+ for uv in uv_coords:
+ f.write(f"vt {uv[0]:.6f} {uv[1]:.6f}\n")
+ f.write("\n")
+
+ for i in range(len(vertices)):
+ f.write(f"vn 0.0 0.0 1.0\n")
+ f.write("\n")
+
+ if os.path.exists(mtl_path):
+ f.write("usemtl material_0\n")
+
+ for face in faces:
+ if uv_coords is not None:
+ f.write(
+ f"f {face[0]+1}/{face[0]+1}/{face[0]+1} {face[1]+1}/{face[1]+1}/{face[1]+1} {face[2]+1}/{face[2]+1}/{face[2]+1}\n"
+ )
+ else:
+ f.write(f"f {face[0]+1} {face[1]+1} {face[2]+1}\n")
+
+ print(f"Saved normalized mesh with original textures to: {temp_obj_path}")
+
+ return vertices, faces, uv_coords, face_uvs, face_materials, mtl_lib
+
+ elif file_path.lower().endswith(".obj") and not normalize_like_blender:
+ print(f"Loading OBJ without normalization: {file_path}")
+
+ # Load OBJ manually and apply pre_transform directly
+ vertices = []
+ uv_coords = []
+ faces = []
+ face_uvs = []
+ face_materials = []
+ current_material = None
+ mtl_lib = None
+
+ with open(file_path, "r") as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("mtllib "):
+ mtl_lib = line.split()[1]
+ elif line.startswith("usemtl "):
+ current_material = line.split()[1]
+ elif line.startswith("v "):
+ parts = line.split()
+ vertices.append([float(parts[1]), float(parts[2]), float(parts[3])])
+ elif line.startswith("vt "):
+ parts = line.split()
+ uv_coords.append([float(parts[1]), float(parts[2])])
+ elif line.startswith("f "):
+ parts = line.split()[1:]
+ face_vertices = []
+ face_uv_indices = []
+
+ for part in parts:
+ indices = part.split("/")
+ vertex_idx = int(indices[0]) - 1
+ uv_idx = (
+ int(indices[1]) - 1
+ if len(indices) > 1 and indices[1]
+ else vertex_idx
+ )
+
+ face_vertices.append(vertex_idx)
+ face_uv_indices.append(uv_idx)
+
+ if len(face_vertices) == 3:
+ faces.append(face_vertices)
+ face_uvs.append(face_uv_indices)
+ face_materials.append(current_material)
+ elif len(face_vertices) == 4:
+ faces.append(
+ [face_vertices[0], face_vertices[1], face_vertices[2]]
+ )
+ faces.append(
+ [face_vertices[0], face_vertices[2], face_vertices[3]]
+ )
+ face_uvs.append(
+ [face_uv_indices[0], face_uv_indices[1], face_uv_indices[2]]
+ )
+ face_uvs.append(
+ [face_uv_indices[0], face_uv_indices[2], face_uv_indices[3]]
+ )
+ face_materials.append(current_material)
+ face_materials.append(current_material)
+
+ vertices = np.array(vertices, dtype=np.float32)
+ faces = np.array(faces, dtype=np.int32)
+ uv_coords = np.array(uv_coords, dtype=np.float32) if uv_coords else None
+
+ print(
+ f"Original OBJ range: X=[{np.min(vertices[:, 0]):.3f}, {np.max(vertices[:, 0]):.3f}], Y=[{np.min(vertices[:, 1]):.3f}, {np.max(vertices[:, 1]):.3f}], Z=[{np.min(vertices[:, 2]):.3f}, {np.max(vertices[:, 2]):.3f}]"
+ )
+
+ # Apply pre_transform directly without normalization
+ if pre_transform is not None:
+ transform_matrix = np.array(pre_transform.matrix())
+ vertices_homogeneous = np.column_stack([vertices, np.ones(len(vertices))])
+ vertices_transformed = (transform_matrix @ vertices_homogeneous.T).T
+ vertices = vertices_transformed[:, :3].astype(np.float32)
+ print(f"Applied pre_transform to OBJ mesh")
+ print(
+ f"Transformed range: X=[{np.min(vertices[:, 0]):.3f}, {np.max(vertices[:, 0]):.3f}], Y=[{np.min(vertices[:, 1]):.3f}, {np.max(vertices[:, 1]):.3f}], Z=[{np.min(vertices[:, 2]):.3f}, {np.max(vertices[:, 2]):.3f}]"
+ )
+
+ print(
+ f"Loaded OBJ without normalization: {len(vertices)} vertices, {len(faces)} faces"
+ )
+ return vertices, faces, uv_coords, face_uvs, face_materials, mtl_lib
+
+ io = SimplicialComplexIO(pre_transform)
+ surface_mesh = io.read(file_path)
+
+ positions = surface_mesh.positions().view()
+
+ vertices = []
+ for i in range(len(positions)):
+ pos = positions[i]
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+ if isinstance(pos_list[0], list):
+ vertices.append([pos_list[0][0], pos_list[1][0], pos_list[2][0]])
+ else:
+ vertices.append(pos_list)
+ else:
+ vertices.append([float(pos[0]), float(pos[1]), float(pos[2])])
+
+ vertices = np.array(vertices, dtype=np.float32)
+
+ # Report the range after loading with pre_transform
+ print(f"Loaded {file_path} with pre_transform applied")
+ print(
+ f"Range: X=[{np.min(vertices[:, 0]):.3f}, {np.max(vertices[:, 0]):.3f}], Y=[{np.min(vertices[:, 1]):.3f}, {np.max(vertices[:, 1]):.3f}], Z=[{np.min(vertices[:, 2]):.3f}, {np.max(vertices[:, 2]):.3f}]"
+ )
+
+ if normalize_like_blender and not file_path.lower().endswith(".obj"):
+ print(f"Applying blender-style normalization with scale {transform_scale}")
+
+ bbox_min = np.min(vertices, axis=0)
+ bbox_max = np.max(vertices, axis=0)
+ bbox_size = bbox_max - bbox_min
+
+ print(f"Original bounding box: {bbox_min} to {bbox_max}")
+ print(f"Original size: {bbox_size}")
+
+ max_dimension = np.max(bbox_size)
+ if max_dimension > 0:
+ # Normalize to unit size, then apply the desired scale
+ scale_factor = transform_scale / max_dimension
+ vertices = vertices * scale_factor
+ print(
+ f"Applied scale factor: {scale_factor} (normalization factor: {1.0/max_dimension}, desired scale: {transform_scale})"
+ )
+
+ bbox_min = np.min(vertices, axis=0)
+ bbox_max = np.max(vertices, axis=0)
+ offset = -(bbox_min + bbox_max) / 2.0
+ vertices = vertices + offset
+ print(f"Applied center offset: {offset}")
+
+ print(f"Applied Blender-style normalization with scale {transform_scale}")
+
+ final_bbox_min = np.min(vertices, axis=0)
+ final_bbox_max = np.max(vertices, axis=0)
+ print(f"Final bounding box: {final_bbox_min} to {final_bbox_max}")
+ else:
+ print(
+ f"No additional normalization applied (normalize_like_blender={normalize_like_blender})"
+ )
+
+ triangles = surface_mesh.triangles()
+ faces = []
+ if triangles.size() > 0:
+ tri_topo = view(triangles.topo())
+ for i in range(triangles.size()):
+ face = tri_topo[i]
+
+ if hasattr(face, "tolist"):
+ face_list = face.tolist()
+ if isinstance(face_list[0], list):
+ faces.append([face_list[0][0], face_list[1][0], face_list[2][0]])
+ else:
+ faces.append(face_list)
+ else:
+ faces.append([int(face[0]), int(face[1]), int(face[2])])
+
+ faces = np.array(faces, dtype=np.int32)
+
+ print(f"Loaded visual mesh: {len(vertices)} vertices, {len(faces)} faces")
+ print(f"Vertices shape: {vertices.shape}, Faces shape: {faces.shape}")
+
+ return (
+ vertices,
+ faces,
+ None,
+ None,
+ None,
+ None,
+ ) # No UV coordinates, face materials, or MTL lib for non-OBJ files
+
+
+def write_visual_meshes_obj(
+ output_path, visual_meshes, visual_mesh_objects, current_visual_vertices=None
+):
+ if not visual_meshes:
+ return
+
+ output_dir = os.path.dirname(output_path)
+ import glob
+ import shutil
+
+ # Collect per-object material information
+ object_materials = {}
+ texture_patterns = ["*.png", "*.jpg", "*.jpeg", "*.tif", "*.tiff", "*.bmp"]
+
+ # Process each object's materials and textures
+ for i, (
+ original_vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_name,
+ face_materials,
+ mtl_lib,
+ ) in enumerate(visual_meshes):
+ object_materials[i] = {
+ "materials": {}, # Will hold material_name -> material_properties
+ "face_materials": face_materials,
+ "obj_name": obj_name,
+ }
+
+ if material_dir and os.path.exists(material_dir):
+ print(
+ f"Processing materials for object '{obj_name}' from directory: {material_dir}"
+ )
+
+ # Find and read MTL file
+ mtl_files = glob.glob(os.path.join(material_dir, "*.mtl"))
+ if mtl_files:
+ mtl_file = mtl_files[0]
+ print(
+ f"Reading material properties for {obj_name} from: {os.path.basename(mtl_file)}"
+ )
+
+ # Parse MTL file to extract materials
+ try:
+ current_material = None
+ with open(mtl_file, "r") as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("newmtl "):
+ current_material = line.split()[1]
+ # Create unique material name for this object
+ unique_material_name = (
+ f"{obj_name}_{i}_{current_material}"
+ )
+ object_materials[i]["materials"][current_material] = {
+ "unique_name": unique_material_name,
+ "properties": [f"newmtl {unique_material_name}"],
+ }
+ elif current_material and (
+ line.startswith("Ka ")
+ or line.startswith("Kd ")
+ or line.startswith("Ks ")
+ or line.startswith("Ns ")
+ or line.startswith("d ")
+ or line.startswith("Tr ")
+ or line.startswith("illum ")
+ or line.startswith("Ni ")
+ ):
+ # Store material properties
+ object_materials[i]["materials"][current_material][
+ "properties"
+ ].append(line)
+ elif current_material and line.startswith("map_"):
+ # Handle texture maps
+ parts = line.split()
+ if len(parts) >= 2:
+ map_type = parts[0]
+ original_texture = parts[1]
+ texture_basename = os.path.basename(
+ original_texture
+ )
+
+ # Create unique texture name for this object
+ unique_texture_name = (
+ f"{obj_name}_{i}_{texture_basename}"
+ )
+
+ # Copy texture file
+ original_texture_path = os.path.join(
+ material_dir, texture_basename
+ )
+ if os.path.exists(original_texture_path):
+ dest_file = os.path.join(
+ output_dir, unique_texture_name
+ )
+ if not os.path.exists(dest_file):
+ shutil.copy2(
+ original_texture_path, dest_file
+ )
+ print(
+ f"Copied texture for {obj_name}: {texture_basename} -> {unique_texture_name}"
+ )
+
+ # Store updated texture reference
+ updated_line = f"{map_type} {unique_texture_name}"
+ object_materials[i]["materials"][current_material][
+ "properties"
+ ].append(updated_line)
+
+ except Exception as e:
+ print(f"Warning: Could not read MTL file for {obj_name}: {e}")
+
+ # Also copy any loose texture files not referenced in MTL
+ for pattern in texture_patterns:
+ for texture_file in glob.glob(os.path.join(material_dir, pattern)):
+ texture_basename = os.path.basename(texture_file)
+ unique_texture_name = f"{obj_name}_{i}_{texture_basename}"
+ dest_file = os.path.join(output_dir, unique_texture_name)
+
+ if not os.path.exists(dest_file):
+ shutil.copy2(texture_file, dest_file)
+ print(
+ f"Copied additional texture for {obj_name}: {texture_basename} -> {unique_texture_name}"
+ )
+
+ # If no materials were found, create a default material
+ if not object_materials[i]["materials"]:
+ default_material_name = f"{obj_name}_{i}_default"
+ object_materials[i]["materials"]["default"] = {
+ "unique_name": default_material_name,
+ "properties": [
+ f"newmtl {default_material_name}",
+ "Ka 0.2 0.2 0.2",
+ "Kd 0.8 0.8 0.8",
+ "Ks 0.0 0.0 0.0",
+ "Ns 0.0",
+ ],
+ }
+ print(f"No materials found for object '{obj_name}', using default material")
+
+ # Write the combined MTL file
+ if object_materials:
+ mtl_path = os.path.join(output_dir, "surface_mesh.mtl")
+ with open(mtl_path, "w") as mtl_f:
+ for i, obj_materials in object_materials.items():
+ obj_name = obj_materials["obj_name"]
+ for material_name, material_info in obj_materials["materials"].items():
+ # Write material properties
+ for prop in material_info["properties"]:
+ mtl_f.write(prop + "\n")
+ mtl_f.write("\n")
+ print(
+ f"Created combined MTL file with materials from {len(object_materials)} objects: {mtl_path}"
+ )
+
+ # Write the OBJ file
+ with open(output_path, "w") as f:
+ # Write MTL library reference
+ if object_materials:
+ f.write("mtllib surface_mesh.mtl\n\n")
+
+ all_vertices = []
+ all_faces = []
+ vertex_offset = 0
+
+ # Collect all vertices and faces first
+ for i, (
+ original_vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_name,
+ face_materials,
+ mtl_lib,
+ ) in enumerate(visual_meshes):
+
+ if current_visual_vertices and i < len(current_visual_vertices):
+ vertices_to_write = current_visual_vertices[i]
+ else:
+ vertices_to_write = original_vertices + centroid_offset
+
+ all_vertices.extend(vertices_to_write)
+
+ for face in faces:
+ adjusted_face = [
+ face[0] + vertex_offset,
+ face[1] + vertex_offset,
+ face[2] + vertex_offset,
+ ]
+ all_faces.append(adjusted_face)
+
+ vertex_offset += len(vertices_to_write)
+
+ # Write vertices
+ for vertex in all_vertices:
+ f.write(f"v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n")
+ f.write("\n")
+
+ # Collect UV coordinates from all meshes and track offsets
+ all_uvs = []
+ mesh_uv_offsets = []
+
+ for i, (
+ original_vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_name,
+ face_materials,
+ mtl_lib,
+ ) in enumerate(visual_meshes):
+ mesh_uv_offsets.append(len(all_uvs)) # Remember where this mesh's UVs start
+
+ if uv_coords is not None and len(uv_coords) > 0:
+ all_uvs.extend(uv_coords)
+ else:
+ # Add dummy UVs for meshes without UV coordinates
+ num_vertices = (
+ len(current_visual_vertices[i])
+ if current_visual_vertices and i < len(current_visual_vertices)
+ else len(original_vertices)
+ )
+ all_uvs.extend([(0.5, 0.5)] * num_vertices)
+
+ # Write UV coordinates
+ for uv in all_uvs:
+ f.write(f"vt {uv[0]:.6f} {uv[1]:.6f}\n")
+ f.write("\n")
+
+ # Write normals
+ for i in range(len(all_vertices)):
+ f.write(f"vn 0.0 0.0 1.0\n")
+ f.write("\n")
+
+ # Write faces with per-object materials, preserving original material assignments
+ vertex_offset = 0
+ face_idx = 0
+
+ for mesh_i, (
+ original_vertices,
+ faces,
+ centroid_offset,
+ physics_obj_index,
+ uv_coords,
+ face_uvs,
+ material_dir,
+ obj_name,
+ face_materials,
+ mtl_lib,
+ ) in enumerate(visual_meshes):
+
+ num_vertices = (
+ len(current_visual_vertices[mesh_i])
+ if current_visual_vertices and mesh_i < len(current_visual_vertices)
+ else len(original_vertices)
+ )
+ uv_offset = mesh_uv_offsets[mesh_i]
+
+ # Group faces by material to minimize usemtl calls
+ material_face_groups = {}
+
+ for face_local_idx in range(len(faces)):
+ face_global_idx = face_idx + face_local_idx
+
+ # Determine material for this face
+ if (
+ face_materials
+ and face_local_idx < len(face_materials)
+ and face_materials[face_local_idx]
+ ):
+ original_material = face_materials[face_local_idx]
+ if (
+ mesh_i in object_materials
+ and original_material in object_materials[mesh_i]["materials"]
+ ):
+ material_name = object_materials[mesh_i]["materials"][
+ original_material
+ ]["unique_name"]
+ else:
+ # Fallback if material not found
+ material_name = f"{obj_name}_{mesh_i}_default"
+ else:
+ # Use default material if no material specified
+ if mesh_i in object_materials:
+ materials_list = list(
+ object_materials[mesh_i]["materials"].keys()
+ )
+ if materials_list:
+ first_material = materials_list[0]
+ material_name = object_materials[mesh_i]["materials"][
+ first_material
+ ]["unique_name"]
+ else:
+ material_name = f"{obj_name}_{mesh_i}_default"
+ else:
+ material_name = f"{obj_name}_{mesh_i}_default"
+
+ if material_name not in material_face_groups:
+ material_face_groups[material_name] = []
+ material_face_groups[material_name].append(face_local_idx)
+
+ # Write faces grouped by material
+ for material_name, face_indices in material_face_groups.items():
+ f.write(f"usemtl {material_name}\n")
+ print(
+ f"Using material '{material_name}' for {len(face_indices)} faces of object '{obj_name}'"
+ )
+
+ for face_local_idx in face_indices:
+ face = all_faces[
+ face_idx + face_local_idx
+ ] # Global face indices (already offset)
+
+ if face_uvs is not None and face_local_idx < len(face_uvs):
+ # Use the original UV mapping
+ uv_indices = face_uvs[face_local_idx]
+ f.write(
+ f"f {face[0]+1}/{uv_indices[0]+1+uv_offset}/{face[0]+1} {face[1]+1}/{uv_indices[1]+1+uv_offset}/{face[1]+1} {face[2]+1}/{uv_indices[2]+1+uv_offset}/{face[2]+1}\n"
+ )
+ else:
+ # Fallback: assume vertex index = UV index
+ f.write(
+ f"f {face[0]+1}/{face[0]+1}/{face[0]+1} {face[1]+1}/{face[1]+1}/{face[1]+1} {face[2]+1}/{face[2]+1}/{face[2]+1}\n"
+ )
+
+ face_idx += len(faces)
+ vertex_offset += num_vertices
+
+ print(
+ f"Exported {len(visual_meshes)} visual meshes with per-object multi-material textures: {len(all_vertices)} vertices, {len(all_faces)} faces to {output_path}"
+ )
+
+
+class MeshProcessor:
+
+ def __init__(self, scene):
+ self.scene = scene
+ self.snh = StableNeoHookean()
+ self.default_element = scene.contact_tabular().default_element()
+
+ def build_tet_mesh(self, voxel_centers, voxel_size):
+
+ half_size = voxel_size / 2.0
+
+ n_voxels = len(voxel_centers)
+ vertices = np.zeros((n_voxels * 8, 3), dtype=np.float32)
+
+ offsets = np.array(
+ [
+ [-half_size, -half_size, -half_size],
+ [half_size, -half_size, -half_size],
+ [half_size, -half_size, half_size],
+ [-half_size, -half_size, half_size],
+ [-half_size, half_size, -half_size],
+ [half_size, half_size, -half_size],
+ [half_size, half_size, half_size],
+ [-half_size, half_size, half_size],
+ ]
+ )
+
+ for i, center in enumerate(voxel_centers):
+
+ for j in range(8):
+ vertices[i * 8 + j] = center + offsets[j]
+
+ tets = np.zeros((n_voxels * 5, 4), dtype=np.int32)
+
+ tet_patterns = [
+ [0, 1, 2, 4],
+ [1, 2, 4, 5],
+ [2, 4, 5, 6],
+ [0, 2, 3, 4],
+ [2, 3, 4, 7],
+ ]
+
+ for i in range(n_voxels):
+ base_vertex = i * 8
+ for j, pattern in enumerate(tet_patterns):
+
+ tet = [base_vertex + v for v in pattern]
+ tets[i * 5 + j] = tet
+
+ print(
+ f"Created tetrahedral mesh with {len(vertices)} vertices and {len(tets)} tetrahedra"
+ )
+ return vertices, tets
+
+ def build_tet_mesh_from_voxels(self, voxel_centers):
+ from scipy.spatial import Delaunay
+
+ # Estimate actual scale from mesh dimensions (voxel centers are already transformed)
+ mesh_size = np.max(voxel_centers, axis=0) - np.min(voxel_centers, axis=0)
+ avg_mesh_dimension = np.mean(mesh_size)
+
+ # Scale-aware jitter: make jitter proportional to the mesh size
+ jitter_amount = max(
+ 0.01 * avg_mesh_dimension / 10.0, 1e-6
+ ) # Minimum jitter to avoid zero
+ print(f"Mesh dimensions: {mesh_size}, avg: {avg_mesh_dimension:.6f}")
+ print(f"Using jitter amount: {jitter_amount:.6f}")
+ np.random.seed(42)
+ jitter = np.random.normal(0, jitter_amount, voxel_centers.shape)
+ voxel_centers_jittered = voxel_centers + jitter
+
+ tri = Delaunay(voxel_centers_jittered)
+
+ vertices = voxel_centers_jittered.astype(np.float64)
+
+ tets = tri.simplices.astype(np.int32)
+
+ # Scale-aware volume threshold: use a threshold relative to mesh size
+ # Typical tetrahedron volume ~ (edge_length)^3, so threshold ~ (avg_dimension/N^(1/3))^3
+ # where N is number of points
+ typical_edge_length = avg_mesh_dimension / (len(voxel_centers) ** (1 / 3))
+ volume_threshold = max(
+ (typical_edge_length**3) * 1e-6, 1e-12
+ ) # Minimum threshold
+ print(
+ f"Using volume threshold: {volume_threshold:.2e} (typical edge length: {typical_edge_length:.6f})"
+ )
+
+ valid_tets = []
+ volumes = []
+ negative_count = 0
+
+ for tet in tets:
+
+ v0, v1, v2, v3 = vertices[tet]
+
+ mat = np.array([v1 - v0, v2 - v0, v3 - v0])
+ signed_volume = np.linalg.det(mat) / 6.0
+ volumes.append(abs(signed_volume))
+
+ if abs(signed_volume) > volume_threshold:
+
+ if signed_volume < 0:
+ tet = [tet[0], tet[2], tet[1], tet[3]]
+ negative_count += 1
+
+ valid_tets.append(tet)
+
+ valid_tets = np.array(valid_tets, dtype=np.int32)
+
+ if volumes:
+ print(
+ f"Volume statistics: min={np.min(volumes):.2e}, max={np.max(volumes):.2e}, median={np.median(volumes):.2e}"
+ )
+ print(f"Negative volumes corrected: {negative_count}")
+
+ print(
+ f"Created tetrahedral mesh with {len(vertices)} vertices and {len(valid_tets)} tetrahedra"
+ )
+ print(f"Filtered out {len(tets) - len(valid_tets)} degenerate tetrahedra")
+
+ return vertices, valid_tets
+
+ def load_voxel_mesh(
+ self,
+ voxel_path,
+ voxel_size=1.0,
+ scale=1.0,
+ pre_transform=None,
+ max_voxels=15000,
+ ):
+ if pre_transform is None:
+ pre_transform = Transform.Identity()
+ pre_transform.scale(scale)
+
+ io = SimplicialComplexIO(pre_transform)
+ voxel_cloud = io.read(voxel_path)
+
+ voxel_positions = voxel_cloud.positions().view()
+
+ voxel_centers = []
+ for i in range(len(voxel_positions)):
+ pos = voxel_positions[i]
+
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+
+ if isinstance(pos_list[0], list):
+ voxel_centers.append(
+ [pos_list[0][0], pos_list[1][0], pos_list[2][0]]
+ )
+ else:
+ voxel_centers.append(pos_list)
+ else:
+ voxel_centers.append([float(pos[0]), float(pos[1]), float(pos[2])])
+
+ voxel_centers = np.array(voxel_centers, dtype=np.float32)
+
+ print(f"Loaded {len(voxel_centers)} voxel centers from {voxel_path}")
+ print(f"Voxel centers shape: {voxel_centers.shape}")
+
+ if max_voxels is None or max_voxels >= len(voxel_centers):
+
+ voxel_centers_subset = voxel_centers
+ stride = 1
+ print(
+ f"Using ALL {len(voxel_centers_subset)} voxels for maximum resolution"
+ )
+ else:
+
+ target_voxels = min(max_voxels, len(voxel_centers))
+ stride = max(1, len(voxel_centers) // target_voxels)
+ voxel_centers_subset = voxel_centers[::stride]
+
+ print(
+ f"Using {len(voxel_centers_subset)} voxels for tetrahedral mesh (stride={stride})"
+ )
+ print(
+ f"Resolution: {len(voxel_centers_subset)}/{len(voxel_centers)} = {100*len(voxel_centers_subset)/len(voxel_centers):.1f}%"
+ )
+
+ vertices, tets = self.build_tet_mesh_from_voxels(voxel_centers_subset)
+
+ print(f"Vertices shape: {vertices.shape}, Tets shape: {tets.shape}")
+
+ tet_mesh = tetmesh(vertices, tets)
+
+ label_surface(tet_mesh)
+
+ return tet_mesh
+
+ def load_mesh(self, file_path, scale=1.0, pre_transform=None):
+ if pre_transform is None:
+ pre_transform = Transform.Identity()
+ pre_transform.scale(scale)
+
+ io = SimplicialComplexIO(pre_transform)
+ surface_mesh = io.read(file_path)
+
+ label_surface(surface_mesh)
+
+ tet_mesh = tetrahedralize(surface_mesh)
+
+ label_surface(tet_mesh)
+
+ return tet_mesh
+
+ def load_material_properties_from_npz(self, npz_path):
+ if not os.path.exists(npz_path):
+ raise FileNotFoundError(f"Material properties file not found: {npz_path}")
+
+ try:
+ data = np.load(npz_path)
+
+ if "voxel_data" not in data:
+ raise ValueError("NPZ file must contain 'voxel_data' field")
+
+ voxel_data = data["voxel_data"]
+
+ voxel_positions = np.column_stack(
+ [voxel_data["x"], voxel_data["y"], voxel_data["z"]]
+ )
+
+ material_props = {
+ "positions": voxel_positions,
+ "youngs_modulus": voxel_data["youngs_modulus"],
+ "poisson_ratio": voxel_data["poissons_ratio"],
+ "density": voxel_data["density"],
+ }
+
+ print(f"Loaded {len(voxel_data)} voxel material properties from {npz_path}")
+ return material_props
+
+ except Exception as e:
+ raise RuntimeError(
+ f"Error loading material properties from {npz_path}: {e}"
+ )
+
+ def assign_per_tetrahedron_materials(
+ self, mesh, material_props, scale=1.0, rotation=None, translation=None
+ ):
+ from scipy.spatial import cKDTree
+
+ tetrahedra = mesh.tetrahedra()
+ vertices = mesh.vertices()
+
+ num_tetrahedra = tetrahedra.size()
+ num_vertices = vertices.size()
+
+ print(
+ f"Assigning materials to {num_tetrahedra} tetrahedra based on {len(material_props['positions'])} voxels"
+ )
+
+ vertex_positions = mesh.positions().view()
+ tet_connectivity = view(tetrahedra.topo())
+
+ tet_centroids = []
+ for i in range(num_tetrahedra):
+ tet_verts = tet_connectivity[i]
+
+ v0 = np.array(vertex_positions[tet_verts[0]]).flatten()
+ v1 = np.array(vertex_positions[tet_verts[1]]).flatten()
+ v2 = np.array(vertex_positions[tet_verts[2]]).flatten()
+ v3 = np.array(vertex_positions[tet_verts[3]]).flatten()
+
+ centroid = (v0 + v1 + v2 + v3) / 4.0
+ tet_centroids.append(centroid)
+
+ tet_centroids = np.array(tet_centroids)
+ print(f"Tetrahedron centroids shape: {tet_centroids.shape}")
+
+ # Transform material voxel positions to match mesh transformation
+ transformed_voxel_positions = material_props["positions"].copy()
+
+ # 1. Apply scaling
+ if scale != 1.0:
+ print(f"Scaling material voxel positions by factor: {scale}")
+ transformed_voxel_positions *= scale
+
+ # 2. Apply rotation if specified
+ if rotation is not None:
+ import math
+
+ print(f"Rotating material voxel positions by: {rotation} degrees")
+
+ # Create rotation matrix - same order as mesh rotation (Z, Y, X)
+ rotation_matrix = np.eye(3)
+
+ # Z rotation
+ if rotation[2] != 0:
+ angle_rad = math.radians(rotation[2])
+ cos_z, sin_z = math.cos(angle_rad), math.sin(angle_rad)
+ rot_z = np.array([[cos_z, -sin_z, 0], [sin_z, cos_z, 0], [0, 0, 1]])
+ rotation_matrix = rotation_matrix @ rot_z
+
+ # Y rotation
+ if rotation[1] != 0:
+ angle_rad = math.radians(rotation[1])
+ cos_y, sin_y = math.cos(angle_rad), math.sin(angle_rad)
+ rot_y = np.array([[cos_y, 0, sin_y], [0, 1, 0], [-sin_y, 0, cos_y]])
+ rotation_matrix = rotation_matrix @ rot_y
+
+ # X rotation
+ if rotation[0] != 0:
+ angle_rad = math.radians(rotation[0])
+ cos_x, sin_x = math.cos(angle_rad), math.sin(angle_rad)
+ rot_x = np.array([[1, 0, 0], [0, cos_x, -sin_x], [0, sin_x, cos_x]])
+ rotation_matrix = rotation_matrix @ rot_x
+
+ # Apply rotation to voxel positions
+ transformed_voxel_positions = (
+ rotation_matrix @ transformed_voxel_positions.T
+ ).T
+
+ # 3. Apply translation if specified
+ if translation is not None:
+ print(f"Translating material voxel positions by: {translation}")
+ transformed_voxel_positions += np.array(translation)
+
+ print(
+ f"Material voxel positions transformed from {np.mean(material_props['positions'], axis=0)} to {np.mean(transformed_voxel_positions, axis=0)} (centroid)"
+ )
+
+ voxel_tree = cKDTree(transformed_voxel_positions)
+
+ distances, closest_voxel_indices = voxel_tree.query(tet_centroids)
+
+ mu_values = []
+ lambda_values = []
+
+ for i in range(num_tetrahedra):
+ voxel_idx = closest_voxel_indices[i]
+
+ E = float(material_props["youngs_modulus"][voxel_idx])
+ nu = float(material_props["poisson_ratio"][voxel_idx])
+
+ mu = E / (2.0 * (1.0 + nu))
+ lam = (E * nu) / ((1.0 + nu) * (1.0 - 2.0 * nu))
+
+ mu_values.append(mu)
+ lambda_values.append(lam)
+
+ mu_attr = mesh.tetrahedra().find("mu")
+ if not mu_attr:
+ mu_attr = mesh.tetrahedra().create("mu", 0.0)
+
+ lambda_attr = mesh.tetrahedra().find("lambda")
+ if not lambda_attr:
+ lambda_attr = mesh.tetrahedra().create("lambda", 0.0)
+
+ mu_view = view(mu_attr)
+ lambda_view = view(lambda_attr)
+
+ for i in range(num_tetrahedra):
+ mu_view[i] = mu_values[i]
+ lambda_view[i] = lambda_values[i]
+
+ mass_density_attr = mesh.vertices().find("mass_density")
+ if not mass_density_attr:
+ mass_density_attr = mesh.vertices().create("mass_density", 1000.0)
+
+ mass_density_view = view(mass_density_attr)
+
+ vertex_coords = []
+ for i in range(num_vertices):
+ vertex_coords.append(np.array(vertex_positions[i]).flatten())
+ vertex_coords = np.array(vertex_coords)
+
+ _, closest_vertex_voxels = voxel_tree.query(vertex_coords)
+
+ for i in range(num_vertices):
+ voxel_idx = closest_vertex_voxels[i]
+ rho = float(material_props["density"][voxel_idx])
+ mass_density_view[i] = rho
+
+ print(
+ f"Successfully assigned per-tetrahedron materials to {num_tetrahedra} tetrahedra"
+ )
+
+ material_counts = {}
+ for i in range(num_tetrahedra):
+ voxel_idx = closest_voxel_indices[i]
+ E = material_props["youngs_modulus"][voxel_idx]
+ if E not in material_counts:
+ material_counts[E] = 0
+ material_counts[E] += 1
+
+ print(f"VERIFICATION: Per-tetrahedron material distribution:")
+ for E, count in material_counts.items():
+ print(f" {count} tetrahedra with E = {E:.0f} Pa")
+ print(
+ f"VERIFICATION: Total tetrahedra: {sum(material_counts.values())} (should equal {num_tetrahedra})"
+ )
+
+ print(f"Applied Lamรฉ parameters range:")
+ print(
+ f" ฮผ (shear modulus): {np.min(mu_values):.0f} - {np.max(mu_values):.0f} Pa"
+ )
+ print(
+ f" ฮป (first Lamรฉ parameter): {np.min(lambda_values):.0f} - {np.max(lambda_values):.0f} Pa"
+ )
+
+ def apply_material_properties(
+ self,
+ mesh,
+ youngs_modulus=1e4,
+ poisson_ratio=0.45,
+ density=500,
+ material_file=None,
+ scale=1.0,
+ rotation=None,
+ translation=None,
+ ):
+
+ if material_file is not None:
+ print(f"Loading heterogeneous material properties from: {material_file}")
+ material_props = self.load_material_properties_from_npz(material_file)
+
+ representative_E = float(np.mean(material_props["youngs_modulus"]))
+ representative_nu = float(np.mean(material_props["poisson_ratio"]))
+ representative_rho = float(np.mean(material_props["density"]))
+
+ print(
+ f"Applying constitutive model with representative values: E={representative_E:.0f} Pa, ฮฝ={representative_nu:.3f}, ฯ={representative_rho:.0f} kg/mยณ"
+ )
+ moduli = ElasticModuli.youngs_poisson(representative_E, representative_nu)
+ self.snh.apply_to(mesh, moduli=moduli, mass_density=representative_rho)
+
+ print(
+ "Overriding uniform values with per-tetrahedron heterogeneous materials..."
+ )
+ self.assign_per_tetrahedron_materials(
+ mesh, material_props, scale, rotation, translation
+ )
+ print(
+ "Per-tetrahedron mu/lambda attributes now override uniform values during simulation"
+ )
+ else:
+
+ print(
+ f"Applying uniform material properties: E={youngs_modulus:.0f} Pa, ฮฝ={poisson_ratio:.3f}, ฯ={density:.0f} kg/mยณ"
+ )
+ moduli = ElasticModuli.youngs_poisson(youngs_modulus, poisson_ratio)
+ self.snh.apply_to(mesh, moduli=moduli, mass_density=density)
+
+ self.default_element.apply_to(mesh)
+
+ def create_simple_tet_mesh(self):
+
+ Vs = np.array(
+ [
+ [0, 1, 0],
+ [0, 0, 1],
+ [-np.sqrt(3) / 2, 0, -0.5],
+ [np.sqrt(3) / 2, 0, -0.5],
+ ],
+ dtype=np.float64,
+ )
+ Ts = np.array([[0, 1, 2, 3]], dtype=np.int32)
+
+ tet_mesh = tetmesh(Vs, Ts)
+
+ label_surface(tet_mesh)
+
+ return tet_mesh
+
+ def create_objects_from_config(self, objects_config):
+ all_meshes = []
+
+ for i, obj_config in enumerate(objects_config):
+
+ if obj_config.get("type") == "voxel":
+ voxel_path = obj_config["voxel_path"]
+ voxel_size = obj_config.get("voxel_size", 1.0)
+ scale = obj_config.get("scale", 1.0)
+ max_voxels = obj_config.get("max_voxels", 15000)
+
+ mesh = self.load_voxel_mesh(
+ voxel_path, voxel_size, scale, max_voxels=max_voxels
+ )
+ elif obj_config.get("type") == "simple_tet":
+ mesh = self.create_simple_tet_mesh()
+ elif obj_config.get("type") == "msh":
+ msh_path = obj_config["msh_path"]
+ scale = obj_config.get("scale", 1.0)
+
+ mesh = self.load_msh_file(msh_path, scale)
+ else:
+
+ mesh = self.load_mesh(obj_config["path"], obj_config.get("scale", 1.0))
+
+ # Extract transformation parameters from config for material assignment
+ mesh_scale = obj_config.get("scale", 1.0)
+ mesh_rotation = obj_config.get("rotation", None)
+ mesh_translation = obj_config.get("translation", None)
+
+ material = obj_config.get("material", {})
+ material_file = material.get("file", None)
+
+ # Apply material properties with transformation parameters
+ # so materials are oriented correctly before mesh transformation
+ self.apply_material_properties(
+ mesh,
+ material.get("youngs_modulus", 1e4),
+ material.get("poisson_ratio", 0.45),
+ material.get("density", 500),
+ material_file,
+ mesh_scale,
+ mesh_rotation,
+ mesh_translation,
+ )
+
+ # Apply boundary conditions if specified in config
+ if obj_config.get("apply_boundary_conditions", False):
+ fix_percentage = obj_config.get("boundary_fix_percentage", 0.1)
+ self.apply_fixed_boundary_conditions(mesh, fix_percentage)
+ elif obj_config.get("apply_frame_boundary_conditions", False):
+ frame_positions_file = obj_config.get(
+ "frame_positions_file", "assets/armchair/frame_positions_for_bc.npz"
+ )
+ proximity_threshold = obj_config.get("frame_proximity_threshold", 0.1)
+ self.apply_frame_boundary_conditions(
+ mesh, frame_positions_file, proximity_threshold
+ )
+
+ transform = Transform.Identity()
+ if "translation" in obj_config:
+ t = obj_config["translation"]
+ translation_vector = (
+ Vector3.UnitX() * t[0]
+ + Vector3.UnitY() * t[1]
+ + Vector3.UnitZ() * t[2]
+ )
+ transform.translate(translation_vector)
+ if "rotation" in obj_config:
+ r = obj_config["rotation"]
+ # Convert degrees to radians and apply rotations in order: Z, Y, X
+ import math
+
+ if r[2] != 0:
+ angle_rad = math.radians(r[2])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitZ()))
+ if r[1] != 0:
+ angle_rad = math.radians(r[1])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitY()))
+ if r[0] != 0:
+ angle_rad = math.radians(r[0])
+ transform.rotate(AngleAxis(angle_rad, Vector3.UnitX()))
+
+ view(mesh.transforms())[0] = transform.matrix()
+
+ obj_name = obj_config.get("name", f"object_{i+1}")
+ scene_object = self.scene.objects().create(obj_name)
+ scene_object.geometries().create(mesh)
+
+ all_meshes.append((mesh, obj_config))
+
+ return all_meshes
+
+ def create_ground(self, height=0.0):
+ ground_object = self.scene.objects().create("ground")
+ g = ground(height)
+ ground_object.geometries().create(g)
+ return ground_object
+
+ def load_msh_file(self, msh_path, scale=1.0, pre_transform=None):
+ """
+ Load a tetrahedral mesh directly from an MSH file (Gmsh format)
+ """
+ if not os.path.exists(msh_path):
+ raise FileNotFoundError(f"MSH file not found: {msh_path}")
+
+ if pre_transform is None:
+ pre_transform = Transform.Identity()
+ pre_transform.scale(scale)
+
+ print(f"Loading tetrahedral mesh from MSH file: {msh_path}")
+
+ # Load the MSH file using SimplicialComplexIO
+ io = SimplicialComplexIO(pre_transform)
+ tet_mesh = io.read(msh_path)
+
+ # Label surface triangles for rendering
+ label_surface(tet_mesh)
+
+ # Get mesh statistics
+ vertices = tet_mesh.vertices()
+ tetrahedra = tet_mesh.tetrahedra()
+ triangles = tet_mesh.triangles()
+
+ print(
+ f"Loaded MSH mesh: {vertices.size()} vertices, {tetrahedra.size()} tetrahedra, {triangles.size()} triangles"
+ )
+ print(f"MSH file loaded successfully from {msh_path}")
+
+ return tet_mesh
+
+ def apply_fixed_boundary_conditions(self, mesh, fix_lower_percentage=0.1):
+ """
+ Apply boundary conditions to fix the lower percentage of vertices to their current positions.
+
+ Args:
+ mesh: The tetrahedral mesh to apply boundary conditions to
+ fix_lower_percentage: Percentage of lowest vertices to fix (default: 0.1 for 10%)
+ """
+ print(
+ f"Applying boundary conditions to fix lower {fix_lower_percentage*100:.1f}% of vertices..."
+ )
+
+ # Get vertex positions
+ vertex_positions = mesh.positions().view()
+ num_vertices = len(vertex_positions)
+
+ # Extract Z coordinates (height) for all vertices
+ z_coords = []
+ for i in range(num_vertices):
+ pos = vertex_positions[i]
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+ if isinstance(pos_list[0], list):
+ z_coords.append(pos_list[2][0]) # Z coordinate
+ else:
+ z_coords.append(pos_list[2]) # Z coordinate
+ else:
+ z_coords.append(float(pos[2])) # Z coordinate
+
+ z_coords = np.array(z_coords)
+
+ # Find the threshold Z coordinate for the lower percentage of vertices
+ z_threshold = np.percentile(z_coords, fix_lower_percentage * 100)
+
+ # Find vertices below or at the threshold
+ fixed_vertex_indices = np.where(z_coords <= z_threshold)[0]
+
+ print(f"Z-coordinate range: {np.min(z_coords):.6f} to {np.max(z_coords):.6f}")
+ print(f"Threshold Z-coordinate: {z_threshold:.6f}")
+ print(
+ f"Fixing {len(fixed_vertex_indices)} vertices out of {num_vertices} total vertices"
+ )
+
+ # Get or create the is_fixed attribute
+ is_fixed_attr = mesh.vertices().find(builtin.is_fixed)
+ if not is_fixed_attr:
+ is_fixed_attr = mesh.vertices().create(builtin.is_fixed, 0)
+
+ # Set the is_fixed attribute for the selected vertices
+ is_fixed_view = view(is_fixed_attr)
+ for vertex_idx in fixed_vertex_indices:
+ is_fixed_view[vertex_idx] = 1
+
+ print(
+ f"Successfully applied boundary conditions to {len(fixed_vertex_indices)} vertices"
+ )
+
+ return fixed_vertex_indices
+
+ def apply_frame_boundary_conditions(
+ self, mesh, frame_positions_file, proximity_threshold=0.1
+ ):
+ """
+ Apply boundary conditions to fix vertices near frame positions.
+
+ Args:
+ mesh: The tetrahedral mesh to apply boundary conditions to
+ frame_positions_file: NPZ file containing frame_positions array
+ proximity_threshold: Distance threshold for considering vertices as frame (default: 0.1)
+ """
+ print(
+ f"Applying frame-based boundary conditions with proximity threshold {proximity_threshold}..."
+ )
+
+ # Load frame positions
+ frame_data = np.load(frame_positions_file)
+ frame_positions = frame_data["frame_positions"]
+ print(f"Loaded {len(frame_positions)} frame voxel positions")
+
+ # Get vertex positions
+ vertex_positions = mesh.positions().view()
+ num_vertices = len(vertex_positions)
+
+ # Convert vertex positions to numpy array
+ vertex_coords = []
+ for i in range(num_vertices):
+ pos = vertex_positions[i]
+ if hasattr(pos, "tolist"):
+ pos_list = pos.tolist()
+ if isinstance(pos_list[0], list):
+ vertex_coords.append(
+ [pos_list[0][0], pos_list[1][0], pos_list[2][0]]
+ )
+ else:
+ vertex_coords.append(pos_list)
+ else:
+ vertex_coords.append([float(pos[0]), float(pos[1]), float(pos[2])])
+
+ vertex_coords = np.array(vertex_coords)
+
+ # Find vertices close to frame positions using KDTree for efficiency
+ from scipy.spatial import cKDTree
+
+ frame_tree = cKDTree(frame_positions)
+
+ # Query distances from each vertex to nearest frame position
+ distances, closest_frame_indices = frame_tree.query(vertex_coords)
+
+ # Find vertices within proximity threshold of frame positions
+ frame_vertex_indices = np.where(distances <= proximity_threshold)[0]
+
+ print(
+ f"Found {len(frame_vertex_indices)} vertices within {proximity_threshold} units of frame positions"
+ )
+ print(
+ f"Fixing {len(frame_vertex_indices)}/{num_vertices} vertices ({100*len(frame_vertex_indices)/num_vertices:.1f}%)"
+ )
+
+ # Get or create the is_fixed attribute
+ is_fixed_attr = mesh.vertices().find(builtin.is_fixed)
+ if not is_fixed_attr:
+ is_fixed_attr = mesh.vertices().create(builtin.is_fixed, 0)
+
+ # Set the is_fixed attribute for frame vertices
+ is_fixed_view = view(is_fixed_attr)
+ for vertex_idx in frame_vertex_indices:
+ is_fixed_view[vertex_idx] = 1
+
+ print(
+ f"Successfully applied frame-based boundary conditions to {len(frame_vertex_indices)} vertices"
+ )
+
+ return frame_vertex_indices
diff --git a/deps/vomp/vomp/trainers/__init__.py b/deps/vomp/vomp/trainers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2996c770104adcbc097d13059df220d13c0c5e9c
--- /dev/null
+++ b/deps/vomp/vomp/trainers/__init__.py
@@ -0,0 +1,45 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib
+
+__attributes = {
+ "BasicTrainer": "basic",
+ "SparseStructureVaeTrainer": "vae.sparse_structure_vae",
+}
+
+__submodules = []
+
+__all__ = list(__attributes.keys()) + __submodules
+
+
+def __getattr__(name):
+ if name not in globals():
+ if name in __attributes:
+ module_name = __attributes[name]
+ module = importlib.import_module(f".{module_name}", __name__)
+ globals()[name] = getattr(module, name)
+ elif name in __submodules:
+ module = importlib.import_module(f".{name}", __name__)
+ globals()[name] = module
+ else:
+ raise AttributeError(f"module {__name__} has no attribute {name}")
+ return globals()[name]
+
+
+# For Pylance
+if __name__ == "__main__":
+ from .basic import BasicTrainer
+ from .vae.sparse_structure_vae import SparseStructureVaeTrainer
diff --git a/deps/vomp/vomp/trainers/base.py b/deps/vomp/vomp/trainers/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe4d0e1c6cb4c88744dd7bb4acaaa0f21d89a205
--- /dev/null
+++ b/deps/vomp/vomp/trainers/base.py
@@ -0,0 +1,444 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from abc import abstractmethod
+import os
+import time
+import json
+
+import torch
+import torch.distributed as dist
+from torch.utils.data import DataLoader
+import numpy as np
+from tqdm import tqdm
+
+from torchvision import utils
+from torch.utils.tensorboard import SummaryWriter
+
+from .utils import *
+from ..utils.general_utils import *
+from ..utils.data_utils import recursive_to_device, cycle, ResumableSampler
+
+
+class Trainer:
+ """
+ Base class for training.
+ """
+
+ def __init__(
+ self,
+ models,
+ dataset,
+ *,
+ output_dir,
+ load_dir,
+ step,
+ max_steps,
+ batch_size=None,
+ batch_size_per_gpu=None,
+ batch_split=None,
+ optimizer={},
+ lr_scheduler=None,
+ elastic=None,
+ grad_clip=None,
+ ema_rate=0.9999,
+ fp16_mode="inflat_all",
+ fp16_scale_growth=1e-3,
+ finetune_ckpt=None,
+ log_param_stats=False,
+ prefetch_data=True,
+ i_print=1000,
+ i_log=500,
+ i_sample=10000,
+ i_save=10000,
+ i_ddpcheck=10000,
+ i_eval=1000,
+ **kwargs,
+ ):
+ assert (
+ batch_size is not None or batch_size_per_gpu is not None
+ ), "Either batch_size or batch_size_per_gpu must be specified."
+
+ self.models = models
+ self.dataset = dataset
+ self.batch_split = batch_split if batch_split is not None else 1
+ self.max_steps = max_steps
+ self.optimizer_config = optimizer
+ self.lr_scheduler_config = lr_scheduler
+ self.elastic_controller_config = elastic
+ self.grad_clip = grad_clip
+ self.ema_rate = [ema_rate] if isinstance(ema_rate, float) else ema_rate
+ self.fp16_mode = fp16_mode
+ self.fp16_scale_growth = fp16_scale_growth
+ self.log_param_stats = log_param_stats
+ self.prefetch_data = prefetch_data
+ if self.prefetch_data:
+ self._data_prefetched = None
+
+ self.output_dir = output_dir
+ self.i_print = i_print
+ self.i_log = i_log
+ self.i_sample = i_sample
+ self.i_save = i_save
+ self.i_ddpcheck = i_ddpcheck
+ self.i_eval = i_eval
+
+ if dist.is_initialized():
+ # Multi-GPU params
+ self.world_size = dist.get_world_size()
+ self.rank = dist.get_rank()
+ self.local_rank = dist.get_rank() % torch.cuda.device_count()
+ self.is_master = self.rank == 0
+ else:
+ # Single-GPU params
+ self.world_size = 1
+ self.rank = 0
+ self.local_rank = 0
+ self.is_master = True
+
+ self.batch_size = (
+ batch_size
+ if batch_size_per_gpu is None
+ else batch_size_per_gpu * self.world_size
+ )
+ self.batch_size_per_gpu = (
+ batch_size_per_gpu
+ if batch_size_per_gpu is not None
+ else batch_size // self.world_size
+ )
+ assert (
+ self.batch_size % self.world_size == 0
+ ), "Batch size must be divisible by the number of GPUs."
+ assert (
+ self.batch_size_per_gpu % self.batch_split == 0
+ ), "Batch size per GPU must be divisible by batch split."
+
+ self.init_models_and_more(**kwargs)
+ self.prepare_dataloader(**kwargs)
+
+ # Load checkpoint
+ self.step = 0
+ if load_dir is not None and step is not None:
+ self.load(load_dir, step)
+ elif finetune_ckpt is not None:
+ self.finetune_from(finetune_ckpt)
+
+ if self.is_master:
+ os.makedirs(os.path.join(self.output_dir, "ckpts"), exist_ok=True)
+ os.makedirs(os.path.join(self.output_dir, "samples"), exist_ok=True)
+ self.writer = SummaryWriter(os.path.join(self.output_dir, "tb_logs"))
+
+ if self.world_size > 1:
+ self.check_ddp()
+
+ if self.is_master:
+ print("\n\nTrainer initialized.")
+ print(self)
+
+ @property
+ def device(self):
+ for _, model in self.models.items():
+ if hasattr(model, "device"):
+ return model.device
+ return next(list(self.models.values())[0].parameters()).device
+
+ @abstractmethod
+ def init_models_and_more(self, **kwargs):
+ """
+ Initialize models and more.
+ """
+ pass
+
+ def prepare_dataloader(self, **kwargs):
+ """
+ Prepare dataloader.
+ """
+ self.data_sampler = ResumableSampler(
+ self.dataset,
+ shuffle=True,
+ )
+ self.dataloader = DataLoader(
+ self.dataset,
+ batch_size=self.batch_size_per_gpu,
+ num_workers=16,
+ pin_memory=True,
+ drop_last=True,
+ persistent_workers=True,
+ prefetch_factor=4,
+ collate_fn=(
+ self.dataset.collate_fn if hasattr(self.dataset, "collate_fn") else None
+ ),
+ sampler=self.data_sampler,
+ )
+ self.data_iterator = cycle(self.dataloader)
+
+ @abstractmethod
+ def load(self, load_dir, step=0):
+ """
+ Load a checkpoint.
+ Should be called by all processes.
+ """
+ pass
+
+ @abstractmethod
+ def save(self):
+ """
+ Save a checkpoint.
+ Should be called only by the rank 0 process.
+ """
+ pass
+
+ @abstractmethod
+ def finetune_from(self, finetune_ckpt):
+ """
+ Finetune from a checkpoint.
+ Should be called by all processes.
+ """
+ pass
+
+ @abstractmethod
+ def update_ema(self):
+ """
+ Update exponential moving average.
+ Should only be called by the rank 0 process.
+ """
+ pass
+
+ @abstractmethod
+ def check_ddp(self):
+ """
+ Check if DDP is working properly.
+ Should be called by all process.
+ """
+ pass
+
+ @abstractmethod
+ def training_losses(**mb_data):
+ """
+ Compute training losses.
+ """
+ pass
+
+ def load_data(self):
+ """
+ Load data.
+ """
+ if self.prefetch_data:
+ if self._data_prefetched is None:
+ self._data_prefetched = recursive_to_device(
+ next(self.data_iterator), self.device, non_blocking=True
+ )
+ data = self._data_prefetched
+ self._data_prefetched = recursive_to_device(
+ next(self.data_iterator), self.device, non_blocking=True
+ )
+ else:
+ data = recursive_to_device(
+ next(self.data_iterator), self.device, non_blocking=True
+ )
+
+ # if the data is a dict, we need to split it into multiple dicts with batch_size_per_gpu
+ if isinstance(data, dict):
+ if self.batch_split == 1:
+ data_list = [data]
+ else:
+ batch_size = list(data.values())[0].shape[0]
+ data_list = [
+ {
+ k: v[
+ i
+ * batch_size
+ // self.batch_split : (i + 1)
+ * batch_size
+ // self.batch_split
+ ]
+ for k, v in data.items()
+ }
+ for i in range(self.batch_split)
+ ]
+ elif isinstance(data, list):
+ data_list = data
+ else:
+ raise ValueError("Data must be a dict or a list of dicts.")
+
+ return data_list
+
+ @abstractmethod
+ def run_step(self, data_list):
+ """
+ Run a training step.
+ """
+ pass
+
+ def run(self):
+ """
+ Run training.
+ """
+ if self.is_master:
+ print("\nStarting training...")
+
+ log = []
+ time_last_print = 0.0
+ time_elapsed = 0.0
+
+ # Initialize simple tqdm progress bar (only on master process)
+ if self.is_master:
+ pbar = tqdm(initial=self.step, total=self.max_steps, desc="Training")
+ else:
+ pbar = None
+
+ while self.step < self.max_steps:
+ time_start = time.time()
+
+ data_list = self.load_data()
+ step_log = self.run_step(data_list)
+
+ time_end = time.time()
+ time_elapsed += time_end - time_start
+
+ self.step += 1
+
+ # Update progress bar
+ if pbar is not None:
+ pbar.update(1)
+
+ # Print progress
+ if self.is_master and self.step % self.i_print == 0:
+ speed = self.i_print / (time_elapsed - time_last_print) * 3600
+ columns = [
+ f"Step: {self.step}/{self.max_steps} ({self.step / self.max_steps * 100:.2f}%)",
+ f"Elapsed: {time_elapsed / 3600:.2f} h",
+ f"Speed: {speed:.2f} steps/h",
+ f"ETA: {(self.max_steps - self.step) / speed:.2f} h",
+ ]
+ print(" | ".join([c.ljust(25) for c in columns]), flush=True)
+ time_last_print = time_elapsed
+
+ # Check ddp
+ if (
+ self.world_size > 1
+ and self.i_ddpcheck is not None
+ and self.step % self.i_ddpcheck == 0
+ ):
+ self.check_ddp()
+
+ if self.is_master:
+ log.append((self.step, {}))
+
+ # Log time
+ log[-1][1]["time"] = {
+ "step": time_end - time_start,
+ "elapsed": time_elapsed,
+ }
+
+ # Log losses
+ if step_log is not None:
+ log[-1][1].update(step_log)
+
+ # Log scale
+ if self.fp16_mode == "amp":
+ log[-1][1]["scale"] = self.scaler.get_scale()
+ elif self.fp16_mode == "inflat_all":
+ log[-1][1]["log_scale"] = self.log_scale
+
+ # Save log
+ if self.step % self.i_log == 0:
+ ## save to log file
+ log_str = "\n".join(
+ [f"{step}: {json.dumps(log)}" for step, log in log]
+ )
+ with open(
+ os.path.join(self.output_dir, "log.txt"), "a"
+ ) as log_file:
+ log_file.write(log_str + "\n")
+
+ # show with mlflow
+ log_show = [
+ l for _, l in log if not dict_any(l, lambda x: np.isnan(x))
+ ]
+ log_show = dict_reduce(log_show, lambda x: np.mean(x))
+ log_show = dict_flatten(log_show, sep="/")
+ for key, value in log_show.items():
+ self.writer.add_scalar(key, value, self.step)
+ log = []
+
+ # Run validation
+ if (
+ hasattr(self, "validate")
+ and hasattr(self, "i_eval")
+ and self.step % self.i_eval == 0
+ ):
+ if self.is_master:
+ print(f"Running validation at step {self.step}...")
+
+ # Set all models to eval mode (only on master process)
+ for model in self.models.values():
+ model.eval()
+
+ # Run validation with error handling (only on master process)
+ val_metrics = {}
+ try:
+ val_metrics = self.validate()
+ if val_metrics:
+ print(f"Validation results at step {self.step}:")
+ for key, value in val_metrics.items():
+ print(f" {key}: {value:.6f}")
+ # Log validation metrics to tensorboard
+ log_key = (
+ f"val_loss/{key}"
+ if not key.startswith("val_")
+ else f"val_loss/{key[4:]}"
+ )
+ self.writer.add_scalar(log_key, value, self.step)
+
+ except Exception as e:
+ print(f"Error during validation: {e}")
+ import traceback
+
+ traceback.print_exc()
+
+ # Set all models back to train mode (only on master process)
+ for model in self.models.values():
+ model.train()
+
+ # Save checkpoint
+ if self.step % self.i_save == 0:
+ self.save()
+
+ # Close progress bar
+ if pbar is not None:
+ pbar.close()
+
+ if self.is_master:
+ self.writer.close()
+ print("Training finished.")
+
+ def profile(self, wait=2, warmup=3, active=5):
+ """
+ Profile the training loop.
+ """
+ with torch.profiler.profile(
+ schedule=torch.profiler.schedule(
+ wait=wait, warmup=warmup, active=active, repeat=1
+ ),
+ on_trace_ready=torch.profiler.tensorboard_trace_handler(
+ os.path.join(self.output_dir, "profile")
+ ),
+ profile_memory=True,
+ with_stack=True,
+ ) as prof:
+ for _ in range(wait + warmup + active):
+ self.run_step()
+ prof.step()
diff --git a/deps/vomp/vomp/trainers/basic.py b/deps/vomp/vomp/trainers/basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3cf79a7d5bfa98f32866f369fdddc0d261d5e41
--- /dev/null
+++ b/deps/vomp/vomp/trainers/basic.py
@@ -0,0 +1,573 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import copy
+from functools import partial
+from contextlib import nullcontext
+
+import torch
+import torch.distributed as dist
+from torch.nn.parallel import DistributedDataParallel as DDP
+import numpy as np
+
+from .utils import *
+from .base import Trainer
+from ..utils.general_utils import *
+from ..utils.dist_utils import *
+from ..utils import grad_clip_utils, elastic_utils
+
+
+class BasicTrainer(Trainer):
+ """
+ Trainer for basic training loop.
+
+ Args:
+ models (dict[str, nn.Module]): Models to train.
+ dataset (torch.utils.data.Dataset): Dataset.
+ output_dir (str): Output directory.
+ load_dir (str): Load directory.
+ step (int): Step to load.
+ batch_size (int): Batch size.
+ batch_size_per_gpu (int): Batch size per GPU. If specified, batch_size will be ignored.
+ batch_split (int): Split batch with gradient accumulation.
+ max_steps (int): Max steps.
+ optimizer (dict): Optimizer config.
+ lr_scheduler (dict): Learning rate scheduler config.
+ elastic (dict): Elastic memory management config.
+ grad_clip (float or dict): Gradient clip config.
+ ema_rate (float or list): Exponential moving average rates.
+ fp16_mode (str): FP16 mode.
+ - None: No FP16.
+ - 'inflat_all': Hold a inflated fp32 master param for all params.
+ - 'amp': Automatic mixed precision.
+ fp16_scale_growth (float): Scale growth for FP16 gradient backpropagation.
+ finetune_ckpt (dict): Finetune checkpoint.
+ log_param_stats (bool): Log parameter stats.
+ i_print (int): Print interval.
+ i_log (int): Log interval.
+ i_sample (int): Sample interval.
+ i_save (int): Save interval.
+ i_ddpcheck (int): DDP check interval.
+ """
+
+ def __str__(self):
+ lines = []
+ lines.append(self.__class__.__name__)
+ lines.append(f" - Models:")
+ for name, model in self.models.items():
+ lines.append(f" - {name}: {model.__class__.__name__}")
+ lines.append(f" - Dataset: {indent(str(self.dataset), 2)}")
+ lines.append(f" - Dataloader:")
+ lines.append(f" - Sampler: {self.dataloader.sampler.__class__.__name__}")
+ lines.append(f" - Num workers: {self.dataloader.num_workers}")
+ lines.append(f" - Number of steps: {self.max_steps}")
+ lines.append(f" - Number of GPUs: {self.world_size}")
+ lines.append(f" - Batch size: {self.batch_size}")
+ lines.append(f" - Batch size per GPU: {self.batch_size_per_gpu}")
+ lines.append(f" - Batch split: {self.batch_split}")
+ lines.append(f" - Optimizer: {self.optimizer.__class__.__name__}")
+ lines.append(f' - Learning rate: {self.optimizer.param_groups[0]["lr"]}')
+ if self.lr_scheduler_config is not None:
+ lines.append(f" - LR scheduler: {self.lr_scheduler.__class__.__name__}")
+ if self.elastic_controller_config is not None:
+ lines.append(
+ f" - Elastic memory: {indent(str(self.elastic_controller), 2)}"
+ )
+ if self.grad_clip is not None:
+ lines.append(f" - Gradient clip: {indent(str(self.grad_clip), 2)}")
+ lines.append(f" - EMA rate: {self.ema_rate}")
+ lines.append(f" - FP16 mode: {self.fp16_mode}")
+ return "\n".join(lines)
+
+ def init_models_and_more(self, **kwargs):
+ """
+ Initialize models and more.
+ """
+ if self.world_size > 1:
+ # Prepare distributed data parallel
+ self.training_models = {
+ name: DDP(
+ model,
+ device_ids=[self.local_rank],
+ output_device=self.local_rank,
+ bucket_cap_mb=128,
+ find_unused_parameters=False,
+ )
+ for name, model in self.models.items()
+ }
+ else:
+ self.training_models = self.models
+
+ # Build master params
+ self.model_params = sum(
+ [
+ [p for p in model.parameters() if p.requires_grad]
+ for model in self.models.values()
+ ],
+ [],
+ )
+ if self.fp16_mode == "amp":
+ self.master_params = self.model_params
+ self.scaler = torch.GradScaler() if self.fp16_mode == "amp" else None
+ elif self.fp16_mode == "inflat_all":
+ self.master_params = make_master_params(self.model_params)
+ self.fp16_scale_growth = self.fp16_scale_growth
+ self.log_scale = 20.0
+ elif self.fp16_mode is None:
+ self.master_params = self.model_params
+ else:
+ raise NotImplementedError(f"FP16 mode {self.fp16_mode} is not implemented.")
+
+ # Build EMA params
+ if self.is_master:
+ self.ema_params = [copy.deepcopy(self.master_params) for _ in self.ema_rate]
+
+ # Initialize optimizer
+ if hasattr(torch.optim, self.optimizer_config["name"]):
+ self.optimizer = getattr(torch.optim, self.optimizer_config["name"])(
+ self.master_params, **self.optimizer_config["args"]
+ )
+ else:
+ self.optimizer = globals()[self.optimizer_config["name"]](
+ self.master_params, **self.optimizer_config["args"]
+ )
+
+ # Initalize learning rate scheduler
+ if self.lr_scheduler_config is not None:
+ if hasattr(torch.optim.lr_scheduler, self.lr_scheduler_config["name"]):
+ self.lr_scheduler = getattr(
+ torch.optim.lr_scheduler, self.lr_scheduler_config["name"]
+ )(self.optimizer, **self.lr_scheduler_config["args"])
+ else:
+ self.lr_scheduler = globals()[self.lr_scheduler_config["name"]](
+ self.optimizer, **self.lr_scheduler_config["args"]
+ )
+
+ # Initialize elastic memory controller
+ if self.elastic_controller_config is not None:
+ assert any(
+ [
+ isinstance(
+ model,
+ (elastic_utils.ElasticModule, elastic_utils.ElasticModuleMixin),
+ )
+ for model in self.models.values()
+ ]
+ ), "No elastic module found in models, please inherit from ElasticModule or ElasticModuleMixin"
+ self.elastic_controller = getattr(
+ elastic_utils, self.elastic_controller_config["name"]
+ )(**self.elastic_controller_config["args"])
+ for model in self.models.values():
+ if isinstance(
+ model,
+ (elastic_utils.ElasticModule, elastic_utils.ElasticModuleMixin),
+ ):
+ model.register_memory_controller(self.elastic_controller)
+
+ # Initialize gradient clipper
+ if self.grad_clip is not None:
+ if isinstance(self.grad_clip, (float, int)):
+ self.grad_clip = float(self.grad_clip)
+ else:
+ self.grad_clip = getattr(grad_clip_utils, self.grad_clip["name"])(
+ **self.grad_clip["args"]
+ )
+
+ def _master_params_to_state_dicts(self, master_params):
+ """
+ Convert master params to dict of state_dicts.
+ """
+ if self.fp16_mode == "inflat_all":
+ master_params = unflatten_master_params(self.model_params, master_params)
+ state_dicts = {name: model.state_dict() for name, model in self.models.items()}
+ master_params_names = sum(
+ [
+ [(name, n) for n, p in model.named_parameters() if p.requires_grad]
+ for name, model in self.models.items()
+ ],
+ [],
+ )
+ for i, (model_name, param_name) in enumerate(master_params_names):
+ state_dicts[model_name][param_name] = master_params[i]
+ return state_dicts
+
+ def _state_dicts_to_master_params(self, master_params, state_dicts):
+ """
+ Convert a state_dict to master params.
+ """
+ master_params_names = sum(
+ [
+ [(name, n) for n, p in model.named_parameters() if p.requires_grad]
+ for name, model in self.models.items()
+ ],
+ [],
+ )
+ params = [
+ state_dicts[name][param_name] for name, param_name in master_params_names
+ ]
+ if self.fp16_mode == "inflat_all":
+ model_params_to_master_params(params, master_params)
+ else:
+ for i, param in enumerate(params):
+ master_params[i].data.copy_(param.data)
+
+ def load(self, load_dir, step=0):
+ """
+ Load a checkpoint.
+ Should be called by all processes.
+ """
+ if self.is_master:
+ print(f"\nLoading checkpoint from step {step}...", end="")
+
+ model_ckpts = {}
+ for name, model in self.models.items():
+ model_ckpt = torch.load(
+ read_file_dist(
+ os.path.join(load_dir, "ckpts", f"{name}_step{step:07d}.pt")
+ ),
+ map_location=self.device,
+ weights_only=True,
+ )
+ model_ckpts[name] = model_ckpt
+ model.load_state_dict(model_ckpt)
+ if self.fp16_mode == "inflat_all":
+ model.convert_to_fp16()
+ self._state_dicts_to_master_params(self.master_params, model_ckpts)
+ del model_ckpts
+
+ if self.is_master:
+ for i, ema_rate in enumerate(self.ema_rate):
+ ema_ckpts = {}
+ for name, model in self.models.items():
+ ema_ckpt = torch.load(
+ os.path.join(
+ load_dir, "ckpts", f"{name}_ema{ema_rate}_step{step:07d}.pt"
+ ),
+ map_location=self.device,
+ weights_only=True,
+ )
+ ema_ckpts[name] = ema_ckpt
+ self._state_dicts_to_master_params(self.ema_params[i], ema_ckpts)
+ del ema_ckpts
+
+ misc_ckpt = torch.load(
+ read_file_dist(os.path.join(load_dir, "ckpts", f"misc_step{step:07d}.pt")),
+ map_location=torch.device("cpu"),
+ weights_only=False,
+ )
+ self.optimizer.load_state_dict(misc_ckpt["optimizer"])
+ self.step = misc_ckpt["step"]
+ self.data_sampler.load_state_dict(misc_ckpt["data_sampler"])
+ if self.fp16_mode == "amp":
+ self.scaler.load_state_dict(misc_ckpt["scaler"])
+ elif self.fp16_mode == "inflat_all":
+ self.log_scale = misc_ckpt["log_scale"]
+ if self.lr_scheduler_config is not None:
+ self.lr_scheduler.load_state_dict(misc_ckpt["lr_scheduler"])
+ if self.elastic_controller_config is not None:
+ self.elastic_controller.load_state_dict(misc_ckpt["elastic_controller"])
+ if self.grad_clip is not None and not isinstance(self.grad_clip, float):
+ self.grad_clip.load_state_dict(misc_ckpt["grad_clip"])
+ del misc_ckpt
+
+ if self.world_size > 1:
+ dist.barrier()
+ if self.is_master:
+ print(" Done.")
+
+ if self.world_size > 1:
+ self.check_ddp()
+
+ def save(self):
+ """
+ Save a checkpoint.
+ Should be called only by the rank 0 process.
+ """
+ assert self.is_master, "save() should be called only by the rank 0 process."
+ print(f"\nSaving checkpoint at step {self.step}...", end="")
+
+ model_ckpts = self._master_params_to_state_dicts(self.master_params)
+ for name, model_ckpt in model_ckpts.items():
+ torch.save(
+ model_ckpt,
+ os.path.join(
+ self.output_dir, "ckpts", f"{name}_step{self.step:07d}.pt"
+ ),
+ )
+
+ for i, ema_rate in enumerate(self.ema_rate):
+ ema_ckpts = self._master_params_to_state_dicts(self.ema_params[i])
+ for name, ema_ckpt in ema_ckpts.items():
+ torch.save(
+ ema_ckpt,
+ os.path.join(
+ self.output_dir,
+ "ckpts",
+ f"{name}_ema{ema_rate}_step{self.step:07d}.pt",
+ ),
+ )
+
+ misc_ckpt = {
+ "optimizer": self.optimizer.state_dict(),
+ "step": self.step,
+ "data_sampler": self.data_sampler.state_dict(),
+ }
+ if self.fp16_mode == "amp":
+ misc_ckpt["scaler"] = self.scaler.state_dict()
+ elif self.fp16_mode == "inflat_all":
+ misc_ckpt["log_scale"] = self.log_scale
+ if self.lr_scheduler_config is not None:
+ misc_ckpt["lr_scheduler"] = self.lr_scheduler.state_dict()
+ if self.elastic_controller_config is not None:
+ misc_ckpt["elastic_controller"] = self.elastic_controller.state_dict()
+ if self.grad_clip is not None and not isinstance(self.grad_clip, float):
+ misc_ckpt["grad_clip"] = self.grad_clip.state_dict()
+ torch.save(
+ misc_ckpt,
+ os.path.join(self.output_dir, "ckpts", f"misc_step{self.step:07d}.pt"),
+ )
+ print(" Done.")
+
+ def finetune_from(self, finetune_ckpt):
+ """
+ Finetune from a checkpoint.
+ Should be called by all processes.
+ """
+ if self.is_master:
+ print("\nFinetuning from:")
+ for name, path in finetune_ckpt.items():
+ print(f" - {name}: {path}")
+
+ model_ckpts = {}
+ for name, model in self.models.items():
+ model_state_dict = model.state_dict()
+ if name in finetune_ckpt:
+ model_ckpt = torch.load(
+ read_file_dist(finetune_ckpt[name]),
+ map_location=self.device,
+ weights_only=True,
+ )
+ for k, v in model_ckpt.items():
+ if model_ckpt[k].shape != model_state_dict[k].shape:
+ if self.is_master:
+ print(
+ f"Warning: {k} shape mismatch, {model_ckpt[k].shape} vs {model_state_dict[k].shape}, skipped."
+ )
+ model_ckpt[k] = model_state_dict[k]
+ model_ckpts[name] = model_ckpt
+ model.load_state_dict(model_ckpt)
+ if self.fp16_mode == "inflat_all":
+ model.convert_to_fp16()
+ else:
+ if self.is_master:
+ print(f"Warning: {name} not found in finetune_ckpt, skipped.")
+ model_ckpts[name] = model_state_dict
+ self._state_dicts_to_master_params(self.master_params, model_ckpts)
+ del model_ckpts
+
+ if self.world_size > 1:
+ dist.barrier()
+ if self.is_master:
+ print("Done.")
+
+ if self.world_size > 1:
+ self.check_ddp()
+
+ def update_ema(self):
+ """
+ Update exponential moving average.
+ Should only be called by the rank 0 process.
+ """
+ assert (
+ self.is_master
+ ), "update_ema() should be called only by the rank 0 process."
+ for i, ema_rate in enumerate(self.ema_rate):
+ for master_param, ema_param in zip(self.master_params, self.ema_params[i]):
+ ema_param.detach().mul_(ema_rate).add_(
+ master_param, alpha=1.0 - ema_rate
+ )
+
+ def check_ddp(self):
+ """
+ Check if DDP is working properly.
+ Should be called by all process.
+ """
+ if self.is_master:
+ print("\nPerforming DDP check...")
+
+ if self.is_master:
+ print("Checking if parameters are consistent across processes...")
+ dist.barrier()
+ try:
+ for p in self.master_params:
+ # split to avoid OOM
+ for i in range(0, p.numel(), 10000000):
+ sub_size = min(10000000, p.numel() - i)
+ sub_p = p.detach().view(-1)[i : i + sub_size]
+ # gather from all processes
+ sub_p_gather = [
+ torch.empty_like(sub_p) for _ in range(self.world_size)
+ ]
+ dist.all_gather(sub_p_gather, sub_p)
+ # check if equal
+ assert all(
+ [
+ torch.equal(sub_p, sub_p_gather[i])
+ for i in range(self.world_size)
+ ]
+ ), "parameters are not consistent across processes"
+ except AssertionError as e:
+ if self.is_master:
+ print(f"\n\033[91mError: {e}\033[0m")
+ print("DDP check failed.")
+ raise e
+
+ dist.barrier()
+ if self.is_master:
+ print("Done.")
+
+ def run_step(self, data_list):
+ """
+ Run a training step.
+ """
+ step_log = {"loss": {}, "status": {}}
+ amp_context = (
+ partial(torch.autocast, device_type="cuda")
+ if self.fp16_mode == "amp"
+ else nullcontext
+ )
+ elastic_controller_context = (
+ self.elastic_controller.record
+ if self.elastic_controller_config is not None
+ else nullcontext
+ )
+
+ # Train
+ losses = []
+ statuses = []
+ elastic_controller_logs = []
+ zero_grad(self.model_params)
+ for i, mb_data in enumerate(data_list):
+ ## sync at the end of each batch split
+ sync_contexts = (
+ [self.training_models[name].no_sync for name in self.training_models]
+ if i != len(data_list) - 1 and self.world_size > 1
+ else [nullcontext]
+ )
+ with nested_contexts(*sync_contexts), elastic_controller_context():
+ with amp_context():
+ loss, status = self.training_losses(**mb_data)
+ l = loss["loss"] / len(data_list)
+ ## backward
+ if self.fp16_mode == "amp":
+ self.scaler.scale(l).backward()
+ elif self.fp16_mode == "inflat_all":
+ scaled_l = l * (2**self.log_scale)
+ scaled_l.backward()
+ else:
+ l.backward()
+ ## log
+ losses.append(
+ dict_foreach(
+ loss, lambda x: x.item() if isinstance(x, torch.Tensor) else x
+ )
+ )
+ statuses.append(
+ dict_foreach(
+ status, lambda x: x.item() if isinstance(x, torch.Tensor) else x
+ )
+ )
+ if self.elastic_controller_config is not None:
+ elastic_controller_logs.append(self.elastic_controller.log())
+ ## gradient clip
+ if self.grad_clip is not None:
+ if self.fp16_mode == "amp":
+ self.scaler.unscale_(self.optimizer)
+ elif self.fp16_mode == "inflat_all":
+ model_grads_to_master_grads(self.model_params, self.master_params)
+ self.master_params[0].grad.mul_(1.0 / (2**self.log_scale))
+ if isinstance(self.grad_clip, float):
+ grad_norm = torch.nn.utils.clip_grad_norm_(
+ self.master_params, self.grad_clip
+ )
+ else:
+ grad_norm = self.grad_clip(self.master_params)
+ if torch.isfinite(grad_norm):
+ statuses[-1]["grad_norm"] = grad_norm.item()
+ ## step
+ if self.fp16_mode == "amp":
+ prev_scale = self.scaler.get_scale()
+ self.scaler.step(self.optimizer)
+ self.scaler.update()
+ elif self.fp16_mode == "inflat_all":
+ prev_scale = 2**self.log_scale
+ if not any(not p.grad.isfinite().all() for p in self.model_params):
+ if self.grad_clip is None:
+ model_grads_to_master_grads(self.model_params, self.master_params)
+ self.master_params[0].grad.mul_(1.0 / (2**self.log_scale))
+ self.optimizer.step()
+ master_params_to_model_params(self.model_params, self.master_params)
+ self.log_scale += self.fp16_scale_growth
+ else:
+ self.log_scale -= 1
+ else:
+ prev_scale = 1.0
+ if not any(not p.grad.isfinite().all() for p in self.model_params):
+ self.optimizer.step()
+ else:
+ print(
+ "\n\033[93mWarning: NaN detected in gradients. Skipping update.\033[0m"
+ )
+ ## adjust learning rate
+ if self.lr_scheduler_config is not None:
+ statuses[-1]["lr"] = self.lr_scheduler.get_last_lr()[0]
+ self.lr_scheduler.step()
+
+ # Logs
+ step_log["loss"] = dict_reduce(losses, lambda x: np.mean(x))
+ step_log["status"] = dict_reduce(
+ statuses,
+ lambda x: np.mean(x),
+ special_func={"min": lambda x: np.min(x), "max": lambda x: np.max(x)},
+ )
+ if self.elastic_controller_config is not None:
+ step_log["elastic"] = dict_reduce(
+ elastic_controller_logs, lambda x: np.mean(x)
+ )
+ if self.grad_clip is not None:
+ step_log["grad_clip"] = (
+ self.grad_clip
+ if isinstance(self.grad_clip, float)
+ else self.grad_clip.log()
+ )
+
+ # Check grad and norm of each param
+ if self.log_param_stats:
+ param_norms = {}
+ param_grads = {}
+ for name, param in self.backbone.named_parameters():
+ if param.requires_grad:
+ param_norms[name] = param.norm().item()
+ if param.grad is not None and torch.isfinite(param.grad).all():
+ param_grads[name] = param.grad.norm().item() / prev_scale
+ step_log["param_norms"] = param_norms
+ step_log["param_grads"] = param_grads
+
+ # Update exponential moving average
+ if self.is_master:
+ self.update_ema()
+
+ return step_log
diff --git a/deps/vomp/vomp/trainers/utils.py b/deps/vomp/vomp/trainers/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..74b8584d456c71bc6fea133eca194a9de6b7e8f6
--- /dev/null
+++ b/deps/vomp/vomp/trainers/utils.py
@@ -0,0 +1,168 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch.nn as nn
+
+# FP16 utils
+from torch._utils import _flatten_dense_tensors, _unflatten_dense_tensors
+
+
+def make_master_params(model_params):
+ """
+ Copy model parameters into a inflated tensor of full-precision parameters.
+ """
+ master_params = _flatten_dense_tensors(
+ [param.detach().float() for param in model_params]
+ )
+ master_params = nn.Parameter(master_params)
+ master_params.requires_grad = True
+ return [master_params]
+
+
+def unflatten_master_params(model_params, master_params):
+ """
+ Unflatten the master parameters to look like model_params.
+ """
+ return _unflatten_dense_tensors(master_params[0].detach(), model_params)
+
+
+def model_params_to_master_params(model_params, master_params):
+ """
+ Copy the model parameter data into the master parameters.
+ """
+ master_params[0].detach().copy_(
+ _flatten_dense_tensors([param.detach().float() for param in model_params])
+ )
+
+
+def master_params_to_model_params(model_params, master_params):
+ """
+ Copy the master parameter data back into the model parameters.
+ """
+ for param, master_param in zip(
+ model_params, _unflatten_dense_tensors(master_params[0].detach(), model_params)
+ ):
+ param.detach().copy_(master_param)
+
+
+def model_grads_to_master_grads(model_params, master_params):
+ """
+ Copy the gradients from the model parameters into the master parameters
+ from make_master_params().
+ """
+ master_params[0].grad = _flatten_dense_tensors(
+ [param.grad.data.detach().float() for param in model_params]
+ )
+
+
+def zero_grad(model_params):
+ for param in model_params:
+ if param.grad is not None:
+ if param.grad.grad_fn is not None:
+ param.grad.detach_()
+ else:
+ param.grad.requires_grad_(False)
+ param.grad.zero_()
+
+
+# LR Schedulers
+from torch.optim.lr_scheduler import LambdaLR
+
+
+class LinearWarmupLRScheduler(LambdaLR):
+ def __init__(self, optimizer, warmup_steps, last_epoch=-1):
+ self.warmup_steps = warmup_steps
+ super(LinearWarmupLRScheduler, self).__init__(
+ optimizer, self.lr_lambda, last_epoch=last_epoch
+ )
+
+ def lr_lambda(self, current_step):
+ if current_step < self.warmup_steps:
+ return float(current_step + 1) / self.warmup_steps
+ return 1.0
+
+
+import os
+import torch
+import json
+from typing import Dict, Any, Optional
+
+
+def get_optimizer(config, params):
+ """
+ Create an optimizer from a config dictionary.
+
+ Args:
+ config: A dictionary with "name" and "args" keys
+ params: The parameters to optimize
+
+ Returns:
+ A PyTorch optimizer
+ """
+ name = config.get("name", "Adam")
+ args = config.get("args", {})
+
+ if name == "Adam":
+ return torch.optim.Adam(params, **args)
+ elif name == "AdamW":
+ return torch.optim.AdamW(params, **args)
+ elif name == "SGD":
+ return torch.optim.SGD(params, **args)
+ else:
+ raise ValueError(f"Optimizer {name} not supported")
+
+
+def save_checkpoint(
+ step: int,
+ models: Dict[str, torch.nn.Module],
+ optimizer: torch.optim.Optimizer,
+ save_dir: str,
+ save_name: str = "latest",
+):
+ """
+ Save a checkpoint of the models and optimizer.
+
+ Args:
+ step: Current training step
+ models: Dictionary of models to save
+ optimizer: Optimizer to save
+ save_dir: Directory to save checkpoint
+ save_name: Name of the checkpoint
+ """
+ if not os.path.exists(save_dir):
+ os.makedirs(save_dir, exist_ok=True)
+
+ ckpt_dir = os.path.join(save_dir, "ckpts")
+ if not os.path.exists(ckpt_dir):
+ os.makedirs(ckpt_dir, exist_ok=True)
+
+ # Save models
+ for name, model in models.items():
+ state_dict = model.state_dict()
+ torch.save(
+ state_dict, os.path.join(ckpt_dir, f"{save_name}_{name}_step_{step}.pt")
+ )
+
+ # Save optimizer
+ torch.save(
+ optimizer.state_dict(),
+ os.path.join(ckpt_dir, f"{save_name}_optimizer_step_{step}.pt"),
+ )
+
+ # Save metadata
+ with open(os.path.join(ckpt_dir, f"{save_name}_meta.json"), "w") as f:
+ json.dump({"step": step}, f)
+
+ print(f"Saved checkpoint at step {step}")
diff --git a/deps/vomp/vomp/trainers/vae/__init__.py b/deps/vomp/vomp/trainers/vae/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/trainers/vae/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/trainers/vae/slat_materials.py b/deps/vomp/vomp/trainers/vae/slat_materials.py
new file mode 100644
index 0000000000000000000000000000000000000000..e549948f05a310e2063b8cc60add5944857b2d11
--- /dev/null
+++ b/deps/vomp/vomp/trainers/vae/slat_materials.py
@@ -0,0 +1,703 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import copy
+import torch
+from torch.utils.data import DataLoader
+import numpy as np
+from easydict import EasyDict as edict
+import utils3d.torch
+
+from ..basic import BasicTrainer
+from ...modules.sparse import SparseTensor
+from ...utils.loss_utils import l1_loss, l2_loss, ssim, lpips
+from ...utils.data_utils import recursive_to_device
+
+
+class SLatVaeMaterialsTrainer(BasicTrainer):
+ """
+ Trainer for structured latent VAE for materials.
+
+ Args:
+ models (dict[str, nn.Module]): Models to train.
+ dataset (torch.utils.data.Dataset): Dataset.
+ output_dir (str): Output directory.
+ load_dir (str): Load directory.
+ step (int): Step to load.
+ batch_size (int): Batch size.
+ batch_size_per_gpu (int): Batch size per GPU. If specified, batch_size will be ignored.
+ batch_split (int): Split batch with gradient accumulation.
+ max_steps (int): Max steps.
+ optimizer (dict): Optimizer config.
+ lr_scheduler (dict): Learning rate scheduler config.
+ elastic (dict): Elastic memory management config.
+ grad_clip (float or dict): Gradient clip config.
+ ema_rate (float or list): Exponential moving average rates.
+ fp16_mode (str): FP16 mode.
+ - None: No FP16.
+ - 'inflat_all': Hold a inflated fp32 master param for all params.
+ - 'amp': Automatic mixed precision.
+ fp16_scale_growth (float): Scale growth for FP16 gradient backpropagation.
+ finetune_ckpt (dict): Finetune checkpoint.
+ log_param_stats (bool): Log parameter stats.
+ i_print (int): Print interval.
+ i_log (int): Log interval.
+ i_sample (int): Sample interval.
+ i_save (int): Save interval.
+ i_ddpcheck (int): DDP check interval.
+
+ loss_type (str): Loss type. Can be 'l1', 'l2'
+ lambda_ssim (float): SSIM loss weight.
+ lambda_lpips (float): LPIPS loss weight.
+ lambda_kl (float): KL loss weight.
+ val_dataset (torch.utils.data.Dataset): Validation dataset.
+ i_eval (int): Evaluation interval.
+ trellis_weights_path (str): Path to TRELLIS weights directory.
+ """
+
+ def __init__(
+ self,
+ *args,
+ loss_type: str = "l1",
+ lambda_youngs_modulus: float = 1.0,
+ lambda_poissons_ratio: float = 1.0,
+ lambda_density: float = 1.0,
+ matvae=None, # Add matvae as a separate parameter
+ val_dataset=None, # Add validation dataset
+ i_eval: int = 1000, # Add evaluation interval
+ trellis_weights_path: str = None, # Add path to TRELLIS weights directory
+ training_mode: str = "encoder_only", # Add training mode parameter
+ **kwargs,
+ ):
+ self.trellis_weights_path = trellis_weights_path
+
+ super().__init__(*args, **kwargs)
+
+ self.loss_type = loss_type
+ self.lambda_youngs_modulus = lambda_youngs_modulus
+ self.lambda_poissons_ratio = lambda_poissons_ratio
+ self.lambda_density = lambda_density
+ self.matvae = matvae # Store the frozen matvae model
+ self.val_dataset = val_dataset # Store validation dataset
+ self.i_eval = i_eval # Store evaluation interval
+ self.training_mode = training_mode # Store training mode
+
+ # Validate training mode
+ valid_modes = [
+ "encoder_only",
+ "encoder_decoder_matvae",
+ "encoder_decoder_direct",
+ ]
+ if self.training_mode not in valid_modes:
+ raise ValueError(
+ f"Invalid training_mode: {self.training_mode}. Must be one of {valid_modes}"
+ )
+
+ # Validate model availability based on training mode
+ if self.training_mode in ["encoder_decoder_matvae", "encoder_decoder_direct"]:
+ if "decoder" not in self.models:
+ raise ValueError(
+ f"Training mode '{self.training_mode}' requires a decoder model in config"
+ )
+
+ # Load TRELLIS pre-trained weights only if no checkpoint was loaded
+ if self.trellis_weights_path is not None and self.step == 0:
+ self._load_trellis_pretrained_weights()
+
+ def _load_trellis_pretrained_weights(self):
+ """
+ Load pre-trained TRELLIS weights for the geometry encoder and decoder.
+ """
+ import os
+ from safetensors.torch import load_file
+
+ safetensors_path = os.path.join(
+ self.trellis_weights_path, "ckpts", "slat_enc_swin8_B_64l8_fp16.safetensors"
+ )
+
+ if self.is_master:
+ print(f"Loading TRELLIS pre-trained weights from: {safetensors_path}")
+
+ # Load the safetensors file
+ pretrained_state_dict = load_file(safetensors_path)
+
+ # Load weights for geometry encoder
+ self._load_pretrained_weights_for_model(
+ "geometry_encoder", pretrained_state_dict
+ )
+
+ # Load weights for decoder if it exists
+ if "decoder" in self.models:
+ self._load_pretrained_weights_for_model("decoder", pretrained_state_dict)
+
+ def _load_pretrained_weights_for_model(self, model_name, pretrained_state_dict):
+ """
+ Load pretrained weights for a specific model.
+
+ Args:
+ model_name: Name of the model ("geometry_encoder" or "decoder")
+ pretrained_state_dict: Dictionary of pretrained weights
+ """
+ model = self.models.get(model_name)
+ if model is None:
+ return
+
+ model_state_dict = model.state_dict()
+
+ # Filter to only include parameters that match both name and shape
+ filtered_state_dict = {}
+ loaded_count = 0
+ skipped_count = 0
+
+ for name, param in pretrained_state_dict.items():
+ if name in model_state_dict:
+ if param.shape == model_state_dict[name].shape:
+ filtered_state_dict[name] = param
+ loaded_count += 1
+ if self.is_master:
+ print(f"โ Loading {model_name}: {name} {param.shape}")
+ else:
+ skipped_count += 1
+ if self.is_master:
+ print(
+ f"โ Shape mismatch {model_name}: {name} | TRELLIS: {param.shape} | Model: {model_state_dict[name].shape}"
+ )
+ else:
+ skipped_count += 1
+ if self.is_master:
+ print(f"โ Not in {model_name}: {name} {param.shape}")
+
+ # Load only the filtered weights
+ model.load_state_dict(filtered_state_dict, strict=False)
+
+ if self.is_master:
+ total_pretrained = len(pretrained_state_dict)
+ print(
+ f"Successfully loaded {loaded_count}/{total_pretrained} parameters from FRANKENSTEIN pre-trained weights for {model_name}"
+ )
+ print(
+ f"Skipped {skipped_count} parameters due to name/shape mismatches for {model_name}"
+ )
+ print(f"--- {model_name} pretrained weight loading complete ---\n")
+
+ def training_losses(
+ self,
+ feats: SparseTensor,
+ materials: SparseTensor,
+ return_aux: bool = False,
+ **kwargs,
+ ) -> Tuple[Dict, Dict]:
+ """
+ Compute training losses.
+
+ Args:
+ feats: The [N x * x C] sparse tensor of features.
+ materials: The [N x * x 3] sparse tensor of materials.
+ return_aux: Whether to return auxiliary information.
+
+ Returns:
+ a dict with the key "loss" containing a scalar tensor.
+ may also contain other keys for different terms.
+ """
+ # Step 1: Run features through geometry encoder to get 2D latents per voxel
+ z, mean, logvar = self.training_models["geometry_encoder"](
+ feats, sample_posterior=False, return_raw=True
+ )
+
+ gt_materials = materials.feats # Shape: [num_voxels, 3]
+
+ if self.training_mode == "encoder_only":
+ # Current behavior: encoder โ matvae
+ # Step 2: Extract the 2D latent features
+ latent_2d = z.feats # Shape: [num_voxels, 2]
+
+ # Step 3: Run latents through material VAE decoder
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+
+ # Extract predictions (remove last dimension if it exists)
+ E_pred = (
+ E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ ) # Shape: [total_voxels]
+ nu_pred = (
+ nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ ) # Shape: [total_voxels]
+ rho_pred = (
+ rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+ ) # Shape: [total_voxels]
+
+ elif self.training_mode == "encoder_decoder_matvae":
+ # New mode: encoder โ decoder โ matvae
+ # Step 2: Run latents through decoder to get 2D outputs
+ decoder_output = self.training_models["decoder"](
+ z
+ ) # Should output 2D latents
+ latent_2d = decoder_output.feats # Shape: [num_voxels, 2]
+
+ # Step 3: Run decoder output through material VAE decoder
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+
+ # Extract predictions (remove last dimension if it exists)
+ E_pred = (
+ E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ ) # Shape: [total_voxels]
+ nu_pred = (
+ nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ ) # Shape: [total_voxels]
+ rho_pred = (
+ rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+ ) # Shape: [total_voxels]
+
+ elif self.training_mode == "encoder_decoder_direct":
+ # New mode: encoder โ decoder โ direct 3D outputs
+ # Step 2: Run latents through decoder to get 3D material outputs directly
+ decoder_output = self.training_models["decoder"](
+ z
+ ) # Should output 3D materials
+ material_predictions = decoder_output.feats # Shape: [num_voxels, 3]
+
+ # Extract predictions directly
+ E_pred = material_predictions[:, 0] # Shape: [total_voxels]
+ nu_pred = material_predictions[:, 1] # Shape: [total_voxels]
+ rho_pred = material_predictions[:, 2] # Shape: [total_voxels]
+
+ # Compute reconstruction loss based on loss_type (same for all modes)
+ if self.loss_type == "l1":
+ l1_E = l1_loss(E_pred, gt_materials[:, 0])
+ l1_nu = l1_loss(nu_pred, gt_materials[:, 1])
+ l1_rho = l1_loss(rho_pred, gt_materials[:, 2])
+
+ loss = (
+ self.lambda_youngs_modulus * l1_E
+ + self.lambda_poissons_ratio * l1_nu
+ + self.lambda_density * l1_rho
+ )
+
+ # Individual loss components for logging
+ loss_youngs = l1_E
+ loss_poisson = l1_nu
+ loss_density = l1_rho
+
+ elif self.loss_type == "l2":
+ l2_E = l2_loss(E_pred, gt_materials[:, 0])
+ l2_nu = l2_loss(nu_pred, gt_materials[:, 1])
+ l2_rho = l2_loss(rho_pred, gt_materials[:, 2])
+
+ loss = (
+ self.lambda_youngs_modulus * l2_E
+ + self.lambda_poissons_ratio * l2_nu
+ + self.lambda_density * l2_rho
+ )
+
+ # Individual loss components for logging
+ loss_youngs = l2_E
+ loss_poisson = l2_nu
+ loss_density = l2_rho
+ else:
+ raise ValueError(f"Invalid loss type: {self.loss_type}")
+
+ if return_aux:
+ return loss, {"latent_2d": z}
+
+ # Return in the expected format: (loss_dict, status_dict)
+ loss_dict = {
+ "loss": loss,
+ "loss_youngs": loss_youngs,
+ "loss_poisson": loss_poisson,
+ "loss_density": loss_density,
+ }
+ status_dict = {}
+
+ return loss_dict, status_dict
+
+ @torch.no_grad()
+ def run_snapshot(
+ self,
+ num_samples: int,
+ batch_size: int,
+ verbose: bool = False,
+ ) -> Dict:
+ """
+ Run material property prediction on samples from the dataset.
+
+ Args:
+ num_samples: Number of samples to process
+ batch_size: Batch size for processing
+ verbose: Whether to print verbose output
+
+ Returns:
+ Dictionary containing predicted and ground truth material properties
+ """
+ dataloader = DataLoader(
+ copy.deepcopy(self.dataset),
+ batch_size=batch_size,
+ shuffle=True,
+ num_workers=4, # OPTIMIZED: Use multiple workers instead of 0
+ pin_memory=True, # OPTIMIZED: Add pin_memory for faster GPU transfer
+ persistent_workers=True, # OPTIMIZED: Keep workers alive
+ collate_fn=(
+ self.dataset.collate_fn if hasattr(self.dataset, "collate_fn") else None
+ ),
+ )
+
+ # Material prediction inference
+ ret_dict = {}
+ all_gt_materials = []
+ all_pred_materials = []
+ all_latents = []
+
+ processed_samples = 0
+
+ for data in dataloader:
+ if processed_samples >= num_samples:
+ break
+
+ # Move data to device
+ feats = data["feats"].cuda() if "feats" in data else None
+ materials = data["materials"].cuda() if "materials" in data else None
+
+ if feats is None or materials is None:
+ continue
+
+ if verbose:
+ print(f"Processing batch: {feats.feats.shape[0]} voxels")
+
+ # Step 1: Run through geometry encoder (deterministic for snapshot)
+ z, mean, logvar = self.models["geometry_encoder"](
+ feats, sample_posterior=False, return_raw=True
+ )
+
+ gt_materials = materials.feats # Shape: [num_voxels, 3]
+
+ if self.training_mode == "encoder_only":
+ # Current behavior: encoder โ matvae
+ # Step 2: Extract latents and ground truth
+ latent_2d = z.feats # Shape: [num_voxels, 2]
+
+ # Step 3: Run latents through material VAE decoder
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+
+ # Extract predictions (remove last dimension if it exists)
+ E_pred = (
+ E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ ) # Shape: [total_voxels]
+ nu_pred = (
+ nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ ) # Shape: [total_voxels]
+ rho_pred = (
+ rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+ ) # Shape: [total_voxels]
+
+ elif self.training_mode == "encoder_decoder_matvae":
+ # New mode: encoder โ decoder โ matvae
+ # Step 2: Run latents through decoder to get 2D outputs
+ decoder_output = self.models["decoder"](z) # Should output 2D latents
+ latent_2d = decoder_output.feats # Shape: [num_voxels, 2]
+
+ # Step 3: Run decoder output through material VAE decoder
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+
+ # Extract predictions (remove last dimension if it exists)
+ E_pred = (
+ E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ ) # Shape: [total_voxels]
+ nu_pred = (
+ nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ ) # Shape: [total_voxels]
+ rho_pred = (
+ rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+ ) # Shape: [total_voxels]
+
+ elif self.training_mode == "encoder_decoder_direct":
+ # New mode: encoder โ decoder โ direct 3D outputs
+ # Step 2: Run latents through decoder to get 3D material outputs directly
+ decoder_output = self.models["decoder"](z) # Should output 3D materials
+ material_predictions = decoder_output.feats # Shape: [num_voxels, 3]
+
+ # Extract predictions directly
+ E_pred = material_predictions[:, 0] # Shape: [total_voxels]
+ nu_pred = material_predictions[:, 1] # Shape: [total_voxels]
+ rho_pred = material_predictions[:, 2] # Shape: [total_voxels]
+
+ # Stack predictions [num_voxels, 3]
+ pred_materials = torch.stack([E_pred, nu_pred, rho_pred], dim=-1)
+
+ # Store results
+ all_gt_materials.append(gt_materials.cpu())
+ all_pred_materials.append(pred_materials.cpu())
+ all_latents.append(z.feats.cpu())
+
+ processed_samples += feats.feats.shape[0]
+
+ if verbose:
+ print(f"Processed {processed_samples}/{num_samples} voxels")
+
+ # Concatenate all results
+ all_gt_materials = torch.cat(all_gt_materials, dim=0)[:num_samples]
+ all_pred_materials = torch.cat(all_pred_materials, dim=0)[:num_samples]
+
+ # Return results
+ ret_dict.update(
+ {
+ "gt_materials": {"value": all_gt_materials, "type": "tensor"},
+ "pred_materials": {"value": all_pred_materials, "type": "tensor"},
+ }
+ )
+
+ return ret_dict
+
+ @torch.no_grad()
+ def validate(self, num_samples: int = 1000) -> Dict:
+ """
+ Run validation on the validation dataset.
+
+ Args:
+ num_samples: Maximum number of samples to validate on
+
+ Returns:
+ Dictionary containing validation metrics
+ """
+ if self.val_dataset is None:
+ return {}
+
+ # Only run validation on master process - return empty dict on other processes
+ if not self.is_master:
+ return {}
+
+ val_metrics = {}
+
+ try:
+ # Create validation dataloader without DDP sampler for consistent results
+ val_dataloader = DataLoader(
+ self.val_dataset,
+ batch_size=min(
+ self.batch_size_per_gpu, 2
+ ), # Use smaller batch size for stability
+ shuffle=False,
+ num_workers=4, # OPTIMIZED: Use multiple workers instead of 0
+ pin_memory=True, # OPTIMIZED: Add pin_memory for faster GPU transfer
+ persistent_workers=True, # OPTIMIZED: Keep workers alive
+ collate_fn=(
+ self.val_dataset.collate_fn
+ if hasattr(self.val_dataset, "collate_fn")
+ else None
+ ),
+ )
+
+ total_loss = 0.0
+ total_loss_youngs = 0.0
+ total_loss_poisson = 0.0
+ total_loss_density = 0.0
+
+ all_pred_materials_original = []
+ all_gt_materials_original = []
+ processed_samples = 0
+
+ for data in val_dataloader:
+ # Move data to device
+ data = recursive_to_device(data, self.device, non_blocking=True)
+ feats = data["feats"]
+ materials = data["materials"]
+
+ # Same forward pass as training_losses
+ z, mean, logvar = self.training_models["geometry_encoder"](
+ feats, sample_posterior=False, return_raw=True
+ )
+ gt_materials_normalized = (
+ materials.feats
+ ) # Shape: [num_voxels, 3] (normalized)
+
+ if self.training_mode == "encoder_only":
+ # Current behavior: encoder โ matvae
+ latent_2d = z.feats # Shape: [num_voxels, 2]
+
+ # Get predictions from material VAE
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+
+ # Extract predictions (remove last dimension if it exists)
+ E_pred = (
+ E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ ) # Shape: [total_voxels]
+ nu_pred = (
+ nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ ) # Shape: [total_voxels]
+ rho_pred = (
+ rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+ ) # Shape: [total_voxels]
+
+ elif self.training_mode == "encoder_decoder_matvae":
+ # New mode: encoder โ decoder โ matvae
+ decoder_output = self.models["decoder"](
+ z
+ ) # Should output 2D latents
+ latent_2d = decoder_output.feats # Shape: [num_voxels, 2]
+
+ # Get predictions from material VAE
+ (E_mu, E_logvar), (nu_mu, nu_logvar), (rho_mu, rho_logvar) = (
+ self.matvae.decode(latent_2d)
+ )
+
+ # Extract predictions (remove last dimension if it exists)
+ E_pred = (
+ E_mu.squeeze(-1) if E_mu.dim() > 1 else E_mu
+ ) # Shape: [total_voxels]
+ nu_pred = (
+ nu_mu.squeeze(-1) if nu_mu.dim() > 1 else nu_mu
+ ) # Shape: [total_voxels]
+ rho_pred = (
+ rho_mu.squeeze(-1) if rho_mu.dim() > 1 else rho_mu
+ ) # Shape: [total_voxels]
+
+ elif self.training_mode == "encoder_decoder_direct":
+ # New mode: encoder โ decoder โ direct 3D outputs
+ decoder_output = self.models["decoder"](
+ z
+ ) # Should output 3D materials
+ material_predictions = (
+ decoder_output.feats
+ ) # Shape: [num_voxels, 3]
+
+ # Extract predictions directly
+ E_pred = material_predictions[:, 0] # Shape: [total_voxels]
+ nu_pred = material_predictions[:, 1] # Shape: [total_voxels]
+ rho_pred = material_predictions[:, 2] # Shape: [total_voxels]
+
+ # Compute losses (same as training_losses)
+ if self.loss_type == "l1":
+ l1_E = l1_loss(E_pred, gt_materials_normalized[:, 0])
+ l1_nu = l1_loss(nu_pred, gt_materials_normalized[:, 1])
+ l1_rho = l1_loss(rho_pred, gt_materials_normalized[:, 2])
+
+ loss = (
+ self.lambda_youngs_modulus * l1_E
+ + self.lambda_poissons_ratio * l1_nu
+ + self.lambda_density * l1_rho
+ )
+ loss_youngs = l1_E
+ loss_poisson = l1_nu
+ loss_density = l1_rho
+
+ elif self.loss_type == "l2":
+ l2_E = l2_loss(E_pred, gt_materials_normalized[:, 0])
+ l2_nu = l2_loss(nu_pred, gt_materials_normalized[:, 1])
+ l2_rho = l2_loss(rho_pred, gt_materials_normalized[:, 2])
+
+ loss = (
+ self.lambda_youngs_modulus * l2_E
+ + self.lambda_poissons_ratio * l2_nu
+ + self.lambda_density * l2_rho
+ )
+ loss_youngs = l2_E
+ loss_poisson = l2_nu
+ loss_density = l2_rho
+
+ # Accumulate losses
+ batch_size = feats.feats.shape[0]
+ total_loss += loss.item() * batch_size
+ total_loss_youngs += loss_youngs.item() * batch_size
+ total_loss_poisson += loss_poisson.item() * batch_size
+ total_loss_density += loss_density.item() * batch_size
+
+ # Stack predictions [num_voxels, 3] (normalized)
+ pred_materials_normalized = torch.stack(
+ [E_pred, nu_pred, rho_pred], dim=-1
+ )
+
+ pred_materials_original = self.val_dataset.material_transform.destandardize_and_inverse_transform_tensor(
+ pred_materials_normalized
+ )
+ gt_materials_original = self.val_dataset.material_transform.destandardize_and_inverse_transform_tensor(
+ gt_materials_normalized
+ )
+
+ # Store for relative error computation
+ all_pred_materials_original.append(pred_materials_original.cpu())
+ all_gt_materials_original.append(gt_materials_original.cpu())
+
+ processed_samples += batch_size
+
+ val_metrics = {
+ "val_loss": (
+ total_loss / processed_samples if processed_samples > 0 else 0.0
+ ),
+ "val_loss_youngs": (
+ total_loss_youngs / processed_samples
+ if processed_samples > 0
+ else 0.0
+ ),
+ "val_loss_poisson": (
+ total_loss_poisson / processed_samples
+ if processed_samples > 0
+ else 0.0
+ ),
+ "val_loss_density": (
+ total_loss_density / processed_samples
+ if processed_samples > 0
+ else 0.0
+ ),
+ "val_samples": processed_samples,
+ }
+
+ # Compute relative errors on original scale
+ if all_pred_materials_original and all_gt_materials_original:
+ all_pred_materials = torch.cat(all_pred_materials_original, dim=0)
+ all_gt_materials = torch.cat(all_gt_materials_original, dim=0)
+
+ pred_E = all_pred_materials[:, 0] # Young's modulus
+ pred_nu = all_pred_materials[:, 1] # Poisson's ratio
+ pred_rho = all_pred_materials[:, 2] # Density
+
+ gt_E = all_gt_materials[:, 0]
+ gt_nu = all_gt_materials[:, 1]
+ gt_rho = all_gt_materials[:, 2]
+
+ # Young's modulus relative error in log space
+ log_pred_E = torch.log10(torch.clamp_min(pred_E, 1e-8))
+ log_gt_E = torch.log10(torch.clamp_min(gt_E, 1e-8))
+ rel_error_log_E = torch.abs(log_pred_E - log_gt_E) / torch.abs(log_gt_E)
+ val_metrics["val_rel_error_log_youngs"] = rel_error_log_E.mean().item()
+
+ # Poisson's ratio relative error in original space
+ rel_error_nu = torch.abs(pred_nu - gt_nu) / torch.abs(gt_nu)
+ val_metrics["val_rel_error_poisson"] = rel_error_nu.mean().item()
+
+ # Density relative error in original space
+ rel_error_rho = torch.abs(pred_rho - gt_rho) / torch.abs(gt_rho)
+ val_metrics["val_rel_error_density"] = rel_error_rho.mean().item()
+
+ # Also compute overall relative error (all properties combined)
+ all_rel_errors = torch.cat(
+ [rel_error_log_E, rel_error_nu, rel_error_rho]
+ )
+ val_metrics["val_rel_error_overall"] = all_rel_errors.mean().item()
+
+ except Exception as e:
+ print(f"Error during validation: {e}")
+ import traceback
+
+ traceback.print_exc()
+ val_metrics = {}
+
+ return val_metrics
diff --git a/deps/vomp/vomp/utils/__init__.py b/deps/vomp/vomp/utils/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..3159bfe65645499015bd92609b99d476d69544e9
--- /dev/null
+++ b/deps/vomp/vomp/utils/__init__.py
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/deps/vomp/vomp/utils/data_utils.py b/deps/vomp/vomp/utils/data_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..01e1b98bb528f04c34507e341dc0c648eecc0aca
--- /dev/null
+++ b/deps/vomp/vomp/utils/data_utils.py
@@ -0,0 +1,247 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import math
+import torch
+import numpy as np
+from torch.utils.data import Sampler, Dataset, DataLoader, DistributedSampler
+import torch.distributed as dist
+
+
+def recursive_to_device(
+ data: Any,
+ device: torch.device,
+ non_blocking: bool = False,
+) -> Any:
+ """
+ Recursively move all tensors in a data structure to a device.
+ """
+ if hasattr(data, "to"):
+ return data.to(device, non_blocking=non_blocking)
+ elif isinstance(data, (list, tuple)):
+ return type(data)(recursive_to_device(d, device, non_blocking) for d in data)
+ elif isinstance(data, dict):
+ return {
+ k: recursive_to_device(v, device, non_blocking) for k, v in data.items()
+ }
+ else:
+ return data
+
+
+def load_balanced_group_indices(
+ load: List[int],
+ num_groups: int,
+ equal_size: bool = False,
+) -> List[List[int]]:
+ """
+ Split indices into groups with balanced load.
+ """
+ if equal_size:
+ group_size = len(load) // num_groups
+ indices = np.argsort(load)[::-1]
+ groups = [[] for _ in range(num_groups)]
+ group_load = np.zeros(num_groups)
+ for idx in indices:
+ min_group_idx = np.argmin(group_load)
+ groups[min_group_idx].append(idx)
+ if equal_size and len(groups[min_group_idx]) == group_size:
+ group_load[min_group_idx] = float("inf")
+ else:
+ group_load[min_group_idx] += load[idx]
+ return groups
+
+
+def cycle(data_loader: DataLoader) -> Iterator:
+ while True:
+ for data in data_loader:
+ if isinstance(data_loader.sampler, ResumableSampler):
+ data_loader.sampler.idx += data_loader.batch_size # type: ignore[attr-defined]
+ yield data
+ if isinstance(data_loader.sampler, DistributedSampler):
+ data_loader.sampler.epoch += 1
+ if isinstance(data_loader.sampler, ResumableSampler):
+ data_loader.sampler.epoch += 1
+ data_loader.sampler.idx = 0
+
+
+class ResumableSampler(Sampler):
+ """
+ Distributed sampler that is resumable.
+
+ Args:
+ dataset: Dataset used for sampling.
+ rank (int, optional): Rank of the current process within :attr:`num_replicas`.
+ By default, :attr:`rank` is retrieved from the current distributed
+ group.
+ shuffle (bool, optional): If ``True`` (default), sampler will shuffle the
+ indices.
+ seed (int, optional): random seed used to shuffle the sampler if
+ :attr:`shuffle=True`. This number should be identical across all
+ processes in the distributed group. Default: ``0``.
+ drop_last (bool, optional): if ``True``, then the sampler will drop the
+ tail of the data to make it evenly divisible across the number of
+ replicas. If ``False``, the sampler will add extra indices to make
+ the data evenly divisible across the replicas. Default: ``False``.
+ """
+
+ def __init__(
+ self,
+ dataset: Dataset,
+ shuffle: bool = True,
+ seed: int = 0,
+ drop_last: bool = False,
+ ) -> None:
+ self.dataset = dataset
+ self.epoch = 0
+ self.idx = 0
+ self.drop_last = drop_last
+ self.world_size = dist.get_world_size() if dist.is_initialized() else 1
+ self.rank = dist.get_rank() if dist.is_initialized() else 0
+ # If the dataset length is evenly divisible by # of replicas, then there
+ # is no need to drop any data, since the dataset will be split equally.
+ if self.drop_last and len(self.dataset) % self.world_size != 0: # type: ignore[arg-type]
+ # Split to nearest available length that is evenly divisible.
+ # This is to ensure each rank receives the same amount of data when
+ # using this Sampler.
+ self.num_samples = math.ceil(
+ (len(self.dataset) - self.world_size) / self.world_size # type: ignore[arg-type]
+ )
+ else:
+ self.num_samples = math.ceil(len(self.dataset) / self.world_size) # type: ignore[arg-type]
+ self.total_size = self.num_samples * self.world_size
+ self.shuffle = shuffle
+ self.seed = seed
+
+ def __iter__(self) -> Iterator:
+ if self.shuffle:
+ # deterministically shuffle based on epoch and seed
+ g = torch.Generator()
+ g.manual_seed(self.seed + self.epoch)
+ indices = torch.randperm(len(self.dataset), generator=g).tolist() # type: ignore[arg-type]
+ else:
+ indices = list(range(len(self.dataset))) # type: ignore[arg-type]
+
+ if not self.drop_last:
+ # add extra samples to make it evenly divisible
+ padding_size = self.total_size - len(indices)
+ if padding_size <= len(indices):
+ indices += indices[:padding_size]
+ else:
+ indices += (indices * math.ceil(padding_size / len(indices)))[
+ :padding_size
+ ]
+ else:
+ # remove tail of data to make it evenly divisible.
+ indices = indices[: self.total_size]
+ assert len(indices) == self.total_size
+
+ # subsample
+ indices = indices[self.rank : self.total_size : self.world_size]
+
+ # resume from previous state
+ indices = indices[self.idx :]
+
+ return iter(indices)
+
+ def __len__(self) -> int:
+ return self.num_samples
+
+ def state_dict(self) -> dict[str, int]:
+ return {
+ "epoch": self.epoch,
+ "idx": self.idx,
+ }
+
+ def load_state_dict(self, state_dict):
+ self.epoch = state_dict["epoch"]
+ self.idx = state_dict["idx"]
+
+
+class BalancedResumableSampler(ResumableSampler):
+ """
+ Distributed sampler that is resumable and balances the load among the processes.
+
+ Args:
+ dataset: Dataset used for sampling.
+ rank (int, optional): Rank of the current process within :attr:`num_replicas`.
+ By default, :attr:`rank` is retrieved from the current distributed
+ group.
+ shuffle (bool, optional): If ``True`` (default), sampler will shuffle the
+ indices.
+ seed (int, optional): random seed used to shuffle the sampler if
+ :attr:`shuffle=True`. This number should be identical across all
+ processes in the distributed group. Default: ``0``.
+ drop_last (bool, optional): if ``True``, then the sampler will drop the
+ tail of the data to make it evenly divisible across the number of
+ replicas. If ``False``, the sampler will add extra indices to make
+ the data evenly divisible across the replicas. Default: ``False``.
+ """
+
+ def __init__(
+ self,
+ dataset: Dataset,
+ shuffle: bool = True,
+ seed: int = 0,
+ drop_last: bool = False,
+ batch_size: int = 1,
+ ) -> None:
+ assert hasattr(
+ dataset, "loads"
+ ), 'Dataset must have "loads" attribute to use BalancedResumableSampler'
+ super().__init__(dataset, shuffle, seed, drop_last)
+ self.batch_size = batch_size
+ self.loads = dataset.loads
+
+ def __iter__(self) -> Iterator:
+ if self.shuffle:
+ # deterministically shuffle based on epoch and seed
+ g = torch.Generator()
+ g.manual_seed(self.seed + self.epoch)
+ indices = torch.randperm(len(self.dataset), generator=g).tolist() # type: ignore[arg-type]
+ else:
+ indices = list(range(len(self.dataset))) # type: ignore[arg-type]
+
+ if not self.drop_last:
+ # add extra samples to make it evenly divisible
+ padding_size = self.total_size - len(indices)
+ if padding_size <= len(indices):
+ indices += indices[:padding_size]
+ else:
+ indices += (indices * math.ceil(padding_size / len(indices)))[
+ :padding_size
+ ]
+ else:
+ # remove tail of data to make it evenly divisible.
+ indices = indices[: self.total_size]
+ assert len(indices) == self.total_size
+
+ # balance load among processes
+ num_batches = len(indices) // (self.batch_size * self.world_size)
+ balanced_indices = []
+ for i in range(num_batches):
+ start_idx = i * self.batch_size * self.world_size
+ end_idx = (i + 1) * self.batch_size * self.world_size
+ batch_indices = indices[start_idx:end_idx]
+ batch_loads = [self.loads[idx] for idx in batch_indices]
+ groups = load_balanced_group_indices(
+ batch_loads, self.world_size, equal_size=True
+ )
+ balanced_indices.extend([batch_indices[j] for j in groups[self.rank]])
+
+ # resume from previous state
+ indices = balanced_indices[self.idx :]
+
+ return iter(indices)
diff --git a/deps/vomp/vomp/utils/dist_utils.py b/deps/vomp/vomp/utils/dist_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..26d3be8fa431e02d73656c68542bf58e2fff8b75
--- /dev/null
+++ b/deps/vomp/vomp/utils/dist_utils.py
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import io
+from contextlib import contextmanager
+import torch
+import torch.distributed as dist
+from torch.nn.parallel import DistributedDataParallel as DDP
+
+
+def setup_dist(rank, local_rank, world_size, master_addr, master_port):
+ os.environ["MASTER_ADDR"] = master_addr
+ os.environ["MASTER_PORT"] = master_port
+ os.environ["WORLD_SIZE"] = str(world_size)
+ os.environ["RANK"] = str(rank)
+ os.environ["LOCAL_RANK"] = str(local_rank)
+ torch.cuda.set_device(local_rank)
+ dist.init_process_group("nccl", rank=rank, world_size=world_size)
+
+
+def read_file_dist(path):
+ """
+ Read the binary file distributedly.
+ File is only read once by the rank 0 process and broadcasted to other processes.
+
+ Returns:
+ data (io.BytesIO): The binary data read from the file.
+ """
+ if dist.is_initialized() and dist.get_world_size() > 1:
+ # read file
+ size = torch.LongTensor(1).cuda()
+ if dist.get_rank() == 0:
+ with open(path, "rb") as f:
+ data = f.read()
+ data = torch.ByteTensor(
+ torch.UntypedStorage.from_buffer(data, dtype=torch.uint8)
+ ).cuda()
+ size[0] = data.shape[0]
+ # broadcast size
+ dist.broadcast(size, src=0)
+ if dist.get_rank() != 0:
+ data = torch.ByteTensor(size[0].item()).cuda()
+ # broadcast data
+ dist.broadcast(data, src=0)
+ # convert to io.BytesIO
+ data = data.cpu().numpy().tobytes()
+ data = io.BytesIO(data)
+ return data
+ else:
+ with open(path, "rb") as f:
+ data = f.read()
+ data = io.BytesIO(data)
+ return data
+
+
+def unwrap_dist(model):
+ """
+ Unwrap the model from distributed training.
+ """
+ if isinstance(model, DDP):
+ return model.module
+ return model
+
+
+@contextmanager
+def master_first():
+ """
+ A context manager that ensures master process executes first.
+ """
+ if not dist.is_initialized():
+ yield
+ else:
+ if dist.get_rank() == 0:
+ yield
+ dist.barrier()
+ else:
+ dist.barrier()
+ yield
+
+
+@contextmanager
+def local_master_first():
+ """
+ A context manager that ensures local master process executes first.
+ """
+ if not dist.is_initialized():
+ yield
+ else:
+ if dist.get_rank() % torch.cuda.device_count() == 0:
+ yield
+ dist.barrier()
+ else:
+ dist.barrier()
+ yield
diff --git a/deps/vomp/vomp/utils/elastic_utils.py b/deps/vomp/vomp/utils/elastic_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..7cd0650cd08301ebc5363e584c712eb37663e12c
--- /dev/null
+++ b/deps/vomp/vomp/utils/elastic_utils.py
@@ -0,0 +1,265 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from abc import abstractmethod
+from contextlib import contextmanager
+from typing import Tuple
+import torch
+import torch.nn as nn
+import numpy as np
+
+
+class MemoryController:
+ """
+ Base class for memory management during training.
+ """
+
+ _last_input_size = None
+ _last_mem_ratio = []
+
+ @contextmanager
+ def record(self):
+ pass
+
+ def update_run_states(self, input_size=None, mem_ratio=None):
+ if self._last_input_size is None:
+ self._last_input_size = input_size
+ elif self._last_input_size != input_size:
+ raise ValueError(
+ f"Input size should not change for different ElasticModules."
+ )
+ self._last_mem_ratio.append(mem_ratio)
+
+ @abstractmethod
+ def get_mem_ratio(self, input_size):
+ pass
+
+ @abstractmethod
+ def state_dict(self):
+ pass
+
+ @abstractmethod
+ def log(self):
+ pass
+
+
+class LinearMemoryController(MemoryController):
+ """
+ A simple controller for memory management during training.
+ The memory usage is modeled as a linear function of:
+ - the number of input parameters
+ - the ratio of memory the model use compared to the maximum usage (with no checkpointing)
+ memory_usage = k * input_size * mem_ratio + b
+ The controller keeps track of the memory usage and gives the
+ expected memory ratio to keep the memory usage under a target
+ """
+
+ def __init__(
+ self,
+ buffer_size=1000,
+ update_every=500,
+ target_ratio=0.8,
+ available_memory=None,
+ max_mem_ratio_start=0.1,
+ params=None,
+ device=None,
+ ):
+ self.buffer_size = buffer_size
+ self.update_every = update_every
+ self.target_ratio = target_ratio
+ self.device = device or torch.cuda.current_device()
+ self.available_memory = (
+ available_memory
+ or torch.cuda.get_device_properties(self.device).total_memory / 1024**3
+ )
+
+ self._memory = np.zeros(buffer_size, dtype=np.float32)
+ self._input_size = np.zeros(buffer_size, dtype=np.float32)
+ self._mem_ratio = np.zeros(buffer_size, dtype=np.float32)
+ self._buffer_ptr = 0
+ self._buffer_length = 0
+ self._params = tuple(params) if params is not None else (0.0, 0.0)
+ self._max_mem_ratio = max_mem_ratio_start
+ self.step = 0
+
+ def __repr__(self):
+ return f"LinearMemoryController(target_ratio={self.target_ratio}, available_memory={self.available_memory})"
+
+ def _add_sample(self, memory, input_size, mem_ratio):
+ self._memory[self._buffer_ptr] = memory
+ self._input_size[self._buffer_ptr] = input_size
+ self._mem_ratio[self._buffer_ptr] = mem_ratio
+ self._buffer_ptr = (self._buffer_ptr + 1) % self.buffer_size
+ self._buffer_length = min(self._buffer_length + 1, self.buffer_size)
+
+ @contextmanager
+ def record(self):
+ torch.cuda.reset_peak_memory_stats(self.device)
+ self._last_input_size = None
+ self._last_mem_ratio = []
+ yield
+ self._last_memory = torch.cuda.max_memory_allocated(self.device) / 1024**3
+ self._last_mem_ratio = sum(self._last_mem_ratio) / len(self._last_mem_ratio)
+ self._add_sample(self._last_memory, self._last_input_size, self._last_mem_ratio)
+ self.step += 1
+ if self.step % self.update_every == 0:
+ self._max_mem_ratio = min(1.0, self._max_mem_ratio + 0.1)
+ self._fit_params()
+
+ def _fit_params(self):
+ memory_usage = self._memory[: self._buffer_length]
+ input_size = self._input_size[: self._buffer_length]
+ mem_ratio = self._mem_ratio[: self._buffer_length]
+
+ x = input_size * mem_ratio
+ y = memory_usage
+ k, b = np.polyfit(x, y, 1)
+ self._params = (k, b)
+ # self._visualize()
+
+ def _visualize(self):
+ import matplotlib.pyplot as plt
+
+ memory_usage = self._memory[: self._buffer_length]
+ input_size = self._input_size[: self._buffer_length]
+ mem_ratio = self._mem_ratio[: self._buffer_length]
+ k, b = self._params
+
+ plt.scatter(input_size * mem_ratio, memory_usage, c=mem_ratio, cmap="viridis")
+ x = np.array([0.0, 20000.0])
+ plt.plot(x, k * x + b, c="r")
+ plt.savefig(f"linear_memory_controller_{self.step}.png")
+ plt.cla()
+
+ def get_mem_ratio(self, input_size):
+ k, b = self._params
+ if k == 0:
+ return np.random.rand() * self._max_mem_ratio
+ pred = (self.available_memory * self.target_ratio - b) / (k * input_size)
+ return min(self._max_mem_ratio, max(0.0, pred))
+
+ def state_dict(self):
+ return {
+ "params": self._params,
+ }
+
+ def load_state_dict(self, state_dict):
+ self._params = tuple(state_dict["params"])
+
+ def log(self):
+ return {
+ "params/k": self._params[0],
+ "params/b": self._params[1],
+ "memory": self._last_memory,
+ "input_size": self._last_input_size,
+ "mem_ratio": self._last_mem_ratio,
+ }
+
+
+class ElasticModule(nn.Module):
+ """
+ Module for training with elastic memory management.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self._memory_controller: MemoryController = None
+
+ @abstractmethod
+ def _get_input_size(self, *args, **kwargs) -> int:
+ """
+ Get the size of the input data.
+
+ Returns:
+ int: The size of the input data.
+ """
+ pass
+
+ @abstractmethod
+ def _forward_with_mem_ratio(
+ self, *args, mem_ratio=0.0, **kwargs
+ ) -> Tuple[float, Tuple]:
+ """
+ Forward with a given memory ratio.
+ """
+ pass
+
+ def register_memory_controller(self, memory_controller: MemoryController):
+ self._memory_controller = memory_controller
+
+ def forward(self, *args, **kwargs):
+ if (
+ self._memory_controller is None
+ or not torch.is_grad_enabled()
+ or not self.training
+ ):
+ _, ret = self._forward_with_mem_ratio(*args, **kwargs)
+ else:
+ input_size = self._get_input_size(*args, **kwargs)
+ mem_ratio = self._memory_controller.get_mem_ratio(input_size)
+ mem_ratio, ret = self._forward_with_mem_ratio(
+ *args, mem_ratio=mem_ratio, **kwargs
+ )
+ self._memory_controller.update_run_states(input_size, mem_ratio)
+ return ret
+
+
+class ElasticModuleMixin:
+ """
+ Mixin for training with elastic memory management.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._memory_controller: MemoryController = None
+
+ @abstractmethod
+ def _get_input_size(self, *args, **kwargs) -> int:
+ """
+ Get the size of the input data.
+
+ Returns:
+ int: The size of the input data.
+ """
+ pass
+
+ @abstractmethod
+ @contextmanager
+ def with_mem_ratio(self, mem_ratio=1.0) -> float:
+ """
+ Context manager for training with a reduced memory ratio compared to the full memory usage.
+
+ Returns:
+ float: The exact memory ratio used during the forward pass.
+ """
+ pass
+
+ def register_memory_controller(self, memory_controller: MemoryController):
+ self._memory_controller = memory_controller
+
+ def forward(self, *args, **kwargs):
+ if (
+ self._memory_controller is None
+ or not torch.is_grad_enabled()
+ or not self.training
+ ):
+ ret = super().forward(*args, **kwargs)
+ else:
+ input_size = self._get_input_size(*args, **kwargs)
+ mem_ratio = self._memory_controller.get_mem_ratio(input_size)
+ with self.with_mem_ratio(mem_ratio) as exact_mem_ratio:
+ ret = super().forward(*args, **kwargs)
+ self._memory_controller.update_run_states(input_size, exact_mem_ratio)
+ return ret
diff --git a/deps/vomp/vomp/utils/general_utils.py b/deps/vomp/vomp/utils/general_utils.py
new file mode 100755
index 0000000000000000000000000000000000000000..3710bc8e4d0c0a2538c379c4a07d2d12906dcd70
--- /dev/null
+++ b/deps/vomp/vomp/utils/general_utils.py
@@ -0,0 +1,244 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import numpy as np
+import cv2
+import torch
+import contextlib
+
+
+# Dictionary utils
+def _dict_merge(dicta, dictb, prefix=""):
+ """
+ Merge two dictionaries.
+ """
+ assert isinstance(dicta, dict), "input must be a dictionary"
+ assert isinstance(dictb, dict), "input must be a dictionary"
+ dict_ = {}
+ all_keys = set(dicta.keys()).union(set(dictb.keys()))
+ for key in all_keys:
+ if key in dicta.keys() and key in dictb.keys():
+ if isinstance(dicta[key], dict) and isinstance(dictb[key], dict):
+ dict_[key] = _dict_merge(
+ dicta[key], dictb[key], prefix=f"{prefix}.{key}"
+ )
+ else:
+ raise ValueError(
+ f"Duplicate key {prefix}.{key} found in both dictionaries. Types: {type(dicta[key])}, {type(dictb[key])}"
+ )
+ elif key in dicta.keys():
+ dict_[key] = dicta[key]
+ else:
+ dict_[key] = dictb[key]
+ return dict_
+
+
+def dict_merge(dicta, dictb):
+ """
+ Merge two dictionaries.
+ """
+ return _dict_merge(dicta, dictb, prefix="")
+
+
+def dict_foreach(dic, func, special_func={}):
+ """
+ Recursively apply a function to all non-dictionary leaf values in a dictionary.
+ """
+ assert isinstance(dic, dict), "input must be a dictionary"
+ for key in dic.keys():
+ if isinstance(dic[key], dict):
+ dic[key] = dict_foreach(dic[key], func)
+ else:
+ if key in special_func.keys():
+ dic[key] = special_func[key](dic[key])
+ else:
+ dic[key] = func(dic[key])
+ return dic
+
+
+def dict_reduce(dicts, func, special_func={}):
+ """
+ Reduce a list of dictionaries. Leaf values must be scalars.
+ """
+ assert isinstance(dicts, list), "input must be a list of dictionaries"
+ assert all(
+ [isinstance(d, dict) for d in dicts]
+ ), "input must be a list of dictionaries"
+ assert len(dicts) > 0, "input must be a non-empty list of dictionaries"
+ all_keys = set([key for dict_ in dicts for key in dict_.keys()])
+ reduced_dict = {}
+ for key in all_keys:
+ vlist = [dict_[key] for dict_ in dicts if key in dict_.keys()]
+ if isinstance(vlist[0], dict):
+ reduced_dict[key] = dict_reduce(vlist, func, special_func)
+ else:
+ if key in special_func.keys():
+ reduced_dict[key] = special_func[key](vlist)
+ else:
+ reduced_dict[key] = func(vlist)
+ return reduced_dict
+
+
+def dict_any(dic, func):
+ """
+ Recursively apply a function to all non-dictionary leaf values in a dictionary.
+ """
+ assert isinstance(dic, dict), "input must be a dictionary"
+ for key in dic.keys():
+ if isinstance(dic[key], dict):
+ if dict_any(dic[key], func):
+ return True
+ else:
+ if func(dic[key]):
+ return True
+ return False
+
+
+def dict_all(dic, func):
+ """
+ Recursively apply a function to all non-dictionary leaf values in a dictionary.
+ """
+ assert isinstance(dic, dict), "input must be a dictionary"
+ for key in dic.keys():
+ if isinstance(dic[key], dict):
+ if not dict_all(dic[key], func):
+ return False
+ else:
+ if not func(dic[key]):
+ return False
+ return True
+
+
+def dict_flatten(dic, sep="."):
+ """
+ Flatten a nested dictionary into a dictionary with no nested dictionaries.
+ """
+ assert isinstance(dic, dict), "input must be a dictionary"
+ flat_dict = {}
+ for key in dic.keys():
+ if isinstance(dic[key], dict):
+ sub_dict = dict_flatten(dic[key], sep=sep)
+ for sub_key in sub_dict.keys():
+ flat_dict[str(key) + sep + str(sub_key)] = sub_dict[sub_key]
+ else:
+ flat_dict[key] = dic[key]
+ return flat_dict
+
+
+# Context utils
+@contextlib.contextmanager
+def nested_contexts(*contexts):
+ with contextlib.ExitStack() as stack:
+ for ctx in contexts:
+ stack.enter_context(ctx())
+ yield
+
+
+# Image utils
+def make_grid(images, nrow=None, ncol=None, aspect_ratio=None):
+ num_images = len(images)
+ if nrow is None and ncol is None:
+ if aspect_ratio is not None:
+ nrow = int(np.round(np.sqrt(num_images / aspect_ratio)))
+ else:
+ nrow = int(np.sqrt(num_images))
+ ncol = (num_images + nrow - 1) // nrow
+ elif nrow is None and ncol is not None:
+ nrow = (num_images + ncol - 1) // ncol
+ elif nrow is not None and ncol is None:
+ ncol = (num_images + nrow - 1) // nrow
+ else:
+ assert (
+ nrow * ncol >= num_images
+ ), "nrow * ncol must be greater than or equal to the number of images"
+
+ if images[0].ndim == 2:
+ grid = np.zeros(
+ (nrow * images[0].shape[0], ncol * images[0].shape[1]),
+ dtype=images[0].dtype,
+ )
+ else:
+ grid = np.zeros(
+ (nrow * images[0].shape[0], ncol * images[0].shape[1], images[0].shape[2]),
+ dtype=images[0].dtype,
+ )
+ for i, img in enumerate(images):
+ row = i // ncol
+ col = i % ncol
+ grid[
+ row * img.shape[0] : (row + 1) * img.shape[0],
+ col * img.shape[1] : (col + 1) * img.shape[1],
+ ] = img
+ return grid
+
+
+def notes_on_image(img, notes=None):
+ img = np.pad(img, ((0, 32), (0, 0), (0, 0)), "constant", constant_values=0)
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
+ if notes is not None:
+ img = cv2.putText(
+ img,
+ notes,
+ (0, img.shape[0] - 4),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 1,
+ (255, 255, 255),
+ 1,
+ )
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
+ return img
+
+
+def save_image_with_notes(img, path, notes=None):
+ """
+ Save an image with notes.
+ """
+ if isinstance(img, torch.Tensor):
+ img = img.cpu().numpy().transpose(1, 2, 0)
+ if img.dtype == np.float32 or img.dtype == np.float64:
+ img = np.clip(img * 255, 0, 255).astype(np.uint8)
+ img = notes_on_image(img, notes)
+ cv2.imwrite(path, cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
+
+
+# debug utils
+
+
+def atol(x, y):
+ """
+ Absolute tolerance.
+ """
+ return torch.abs(x - y)
+
+
+def rtol(x, y):
+ """
+ Relative tolerance.
+ """
+ return torch.abs(x - y) / torch.clamp_min(
+ torch.maximum(torch.abs(x), torch.abs(y)), 1e-12
+ )
+
+
+# print utils
+def indent(s, n=4):
+ """
+ Indent a string.
+ """
+ lines = s.split("\n")
+ for i in range(1, len(lines)):
+ lines[i] = " " * n + lines[i]
+ return "\n".join(lines)
diff --git a/deps/vomp/vomp/utils/grad_clip_utils.py b/deps/vomp/vomp/utils/grad_clip_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..b47ce687d43f9c51d96ab2223428c64f8290cea4
--- /dev/null
+++ b/deps/vomp/vomp/utils/grad_clip_utils.py
@@ -0,0 +1,109 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import *
+import torch
+import numpy as np
+import torch.utils
+
+
+class AdaptiveGradClipper:
+ """
+ Adaptive gradient clipping for training.
+ """
+
+ def __init__(
+ self,
+ max_norm=None,
+ clip_percentile=95.0,
+ buffer_size=1000,
+ ):
+ self.max_norm = max_norm
+ self.clip_percentile = clip_percentile
+ self.buffer_size = buffer_size
+
+ self._grad_norm = np.zeros(buffer_size, dtype=np.float32)
+ self._max_norm = max_norm
+ self._buffer_ptr = 0
+ self._buffer_length = 0
+
+ def __repr__(self):
+ return f"AdaptiveGradClipper(max_norm={self.max_norm}, clip_percentile={self.clip_percentile})"
+
+ def state_dict(self):
+ return {
+ "grad_norm": self._grad_norm,
+ "max_norm": self._max_norm,
+ "buffer_ptr": self._buffer_ptr,
+ "buffer_length": self._buffer_length,
+ }
+
+ def load_state_dict(self, state_dict):
+ self._grad_norm = state_dict["grad_norm"]
+ self._max_norm = state_dict["max_norm"]
+ self._buffer_ptr = state_dict["buffer_ptr"]
+ self._buffer_length = state_dict["buffer_length"]
+
+ def log(self):
+ return {
+ "max_norm": self._max_norm,
+ }
+
+ def __call__(
+ self, parameters, norm_type=2.0, error_if_nonfinite=False, foreach=None
+ ):
+ """Clip the gradient norm of an iterable of parameters.
+
+ The norm is computed over all gradients together, as if they were
+ concatenated into a single vector. Gradients are modified in-place.
+
+ Args:
+ parameters (Iterable[Tensor] or Tensor): an iterable of Tensors or a
+ single Tensor that will have gradients normalized
+ norm_type (float): type of the used p-norm. Can be ``'inf'`` for
+ infinity norm.
+ error_if_nonfinite (bool): if True, an error is thrown if the total
+ norm of the gradients from :attr:`parameters` is ``nan``,
+ ``inf``, or ``-inf``. Default: False (will switch to True in the future)
+ foreach (bool): use the faster foreach-based implementation.
+ If ``None``, use the foreach implementation for CUDA and CPU native tensors and silently
+ fall back to the slow implementation for other device types.
+ Default: ``None``
+
+ Returns:
+ Total norm of the parameter gradients (viewed as a single vector).
+ """
+ max_norm = self._max_norm if self._max_norm is not None else float("inf")
+ grad_norm = torch.nn.utils.clip_grad_norm_(
+ parameters,
+ max_norm=max_norm,
+ norm_type=norm_type,
+ error_if_nonfinite=error_if_nonfinite,
+ foreach=foreach,
+ )
+
+ if torch.isfinite(grad_norm):
+ self._grad_norm[self._buffer_ptr] = grad_norm
+ self._buffer_ptr = (self._buffer_ptr + 1) % self.buffer_size
+ self._buffer_length = min(self._buffer_length + 1, self.buffer_size)
+ if self._buffer_length == self.buffer_size:
+ self._max_norm = np.percentile(self._grad_norm, self.clip_percentile)
+ self._max_norm = (
+ min(self._max_norm, self.max_norm)
+ if self.max_norm is not None
+ else self._max_norm
+ )
+
+ return grad_norm
diff --git a/deps/vomp/vomp/utils/loss_utils.py b/deps/vomp/vomp/utils/loss_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac6ee467597834a15d97cd5225b79a70eafc7e0f
--- /dev/null
+++ b/deps/vomp/vomp/utils/loss_utils.py
@@ -0,0 +1,126 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import torch.nn.functional as F
+from torch.autograd import Variable
+from math import exp
+from lpips import LPIPS
+
+
+def smooth_l1_loss(pred, target, beta=1.0):
+ diff = torch.abs(pred - target)
+ loss = torch.where(diff < beta, 0.5 * diff**2 / beta, diff - 0.5 * beta)
+ return loss.mean()
+
+
+def l1_loss(network_output, gt):
+ return torch.abs((network_output - gt)).mean()
+
+
+def l2_loss(network_output, gt):
+ return ((network_output - gt) ** 2).mean()
+
+
+def gaussian(window_size, sigma):
+ gauss = torch.Tensor(
+ [
+ exp(-((x - window_size // 2) ** 2) / float(2 * sigma**2))
+ for x in range(window_size)
+ ]
+ )
+ return gauss / gauss.sum()
+
+
+def create_window(window_size, channel):
+ _1D_window = gaussian(window_size, 1.5).unsqueeze(1)
+ _2D_window = _1D_window.mm(_1D_window.t()).float().unsqueeze(0).unsqueeze(0)
+ window = Variable(
+ _2D_window.expand(channel, 1, window_size, window_size).contiguous()
+ )
+ return window
+
+
+def psnr(img1, img2, max_val=1.0):
+ mse = F.mse_loss(img1, img2)
+ return 20 * torch.log10(max_val / torch.sqrt(mse))
+
+
+def ssim(img1, img2, window_size=11, size_average=True):
+ channel = img1.size(-3)
+ window = create_window(window_size, channel)
+
+ if img1.is_cuda:
+ window = window.cuda(img1.get_device())
+ window = window.type_as(img1)
+
+ return _ssim(img1, img2, window, window_size, channel, size_average)
+
+
+def _ssim(img1, img2, window, window_size, channel, size_average=True):
+ mu1 = F.conv2d(img1, window, padding=window_size // 2, groups=channel)
+ mu2 = F.conv2d(img2, window, padding=window_size // 2, groups=channel)
+
+ mu1_sq = mu1.pow(2)
+ mu2_sq = mu2.pow(2)
+ mu1_mu2 = mu1 * mu2
+
+ sigma1_sq = (
+ F.conv2d(img1 * img1, window, padding=window_size // 2, groups=channel) - mu1_sq
+ )
+ sigma2_sq = (
+ F.conv2d(img2 * img2, window, padding=window_size // 2, groups=channel) - mu2_sq
+ )
+ sigma12 = (
+ F.conv2d(img1 * img2, window, padding=window_size // 2, groups=channel)
+ - mu1_mu2
+ )
+
+ C1 = 0.01**2
+ C2 = 0.03**2
+
+ ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / (
+ (mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)
+ )
+
+ if size_average:
+ return ssim_map.mean()
+ else:
+ return ssim_map.mean(1).mean(1).mean(1)
+
+
+loss_fn_vgg = None
+
+
+def lpips(img1, img2, value_range=(0, 1)):
+ global loss_fn_vgg
+ if loss_fn_vgg is None:
+ loss_fn_vgg = LPIPS(net="vgg").cuda().eval()
+ # normalize to [-1, 1]
+ img1 = (img1 - value_range[0]) / (value_range[1] - value_range[0]) * 2 - 1
+ img2 = (img2 - value_range[0]) / (value_range[1] - value_range[0]) * 2 - 1
+ return loss_fn_vgg(img1, img2).mean()
+
+
+def normal_angle(pred, gt):
+ pred = pred * 2.0 - 1.0
+ gt = gt * 2.0 - 1.0
+ norms = pred.norm(dim=-1) * gt.norm(dim=-1)
+ cos_sim = (pred * gt).sum(-1) / (norms + 1e-9)
+ cos_sim = torch.clamp(cos_sim, -1.0, 1.0)
+ ang = torch.rad2deg(torch.acos(cos_sim[norms > 1e-9])).mean()
+ if ang.isnan():
+ return -1
+ return ang
diff --git a/deps/vomp/vomp/utils/material_transforms.py b/deps/vomp/vomp/utils/material_transforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..68d6a0969a03b649f9f55f9e7c3badbdeb14badb
--- /dev/null
+++ b/deps/vomp/vomp/utils/material_transforms.py
@@ -0,0 +1,672 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Material property transforms for normalization and denormalization."""
+
+import torch
+from typing import Dict, Tuple, Optional
+
+
+class MaterialPropertyTransform:
+ """Handles forward and inverse transforms for material properties.
+
+ This class provides consistent normalization for material properties:
+ - Standard mode: log transform + standardization
+ - Log minmax mode: log transform + min-max normalization
+ """
+
+ def __init__(
+ self,
+ nu_min: float = 0.0,
+ nu_max: float = 0.5,
+ normalization_type: str = "standard",
+ ):
+ """Initialize transform with Poisson's ratio bounds.
+
+ Args:
+ nu_min: Minimum Poisson's ratio (default: 0.0)
+ nu_max: Maximum Poisson's ratio (default: 0.5)
+ normalization_type: Type of normalization ("standard" or "log_minmax")
+ """
+ self.nu_min = nu_min
+ self.nu_max = nu_max
+ self.normalization_type = normalization_type
+ self._stats_computed = False
+
+ # For standard normalization
+ self.mu = None
+ self.std = None
+
+ # For log minmax normalization
+ self.E_min = None
+ self.E_max = None
+ self.rho_min = None
+ self.rho_max = None
+
+ def forward_transform(
+ self, properties: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply forward transform to material properties.
+
+ Args:
+ properties: Dictionary with keys 'youngs_modulus', 'poissons_ratio', 'density'
+
+ Returns:
+ Transformed properties dictionary
+ """
+ if self.normalization_type == "log_minmax":
+ return self._log_minmax_transform(properties)
+ elif self.normalization_type == "log_minmax_no_density":
+ return self._log_minmax_no_density_transform(properties)
+ else:
+ return self._standard_transform(properties)
+
+ def _standard_transform(
+ self, properties: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply standard log/logit transform."""
+ transformed = {}
+
+ # Young's modulus - log transform
+ E = properties["youngs_modulus"]
+ E_clamped = torch.clamp_min(E, 1e-8)
+ transformed["youngs_modulus"] = torch.log10(E_clamped)
+
+ # Poisson's ratio - logit transform with bounds
+ nu = properties["poissons_ratio"]
+ # Normalize to [0, 1] range based on physical bounds
+ p = (nu - self.nu_min) / (self.nu_max - self.nu_min)
+ p_clamped = torch.clamp(p, 1e-4, 1.0 - 1e-4)
+ transformed["poissons_ratio"] = torch.logit(p_clamped)
+
+ # Density - log transform
+ rho = properties["density"]
+ rho_clamped = torch.clamp_min(rho, 1e-8)
+ transformed["density"] = torch.log10(rho_clamped)
+
+ return transformed
+
+ def _log_minmax_transform(
+ self, properties: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply log minmax transform."""
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before log minmax transform")
+
+ transformed = {}
+
+ # Young's modulus - log transform then min-max normalize
+ E = properties["youngs_modulus"]
+ E_clamped = torch.clamp_min(E, 1e-8)
+ log_E = torch.log10(E_clamped)
+ transformed["youngs_modulus"] = (log_E - self.E_min) / (self.E_max - self.E_min)
+
+ # Poisson's ratio - min-max normalize directly
+ nu = properties["poissons_ratio"]
+ transformed["poissons_ratio"] = (nu - self.nu_min) / (self.nu_max - self.nu_min)
+
+ # Density - log transform then min-max normalize
+ rho = properties["density"]
+ rho_clamped = torch.clamp_min(rho, 1e-8)
+ log_rho = torch.log10(rho_clamped)
+ transformed["density"] = (log_rho - self.rho_min) / (
+ self.rho_max - self.rho_min
+ )
+
+ return transformed
+
+ def _log_minmax_no_density_transform(
+ self, properties: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply log minmax transform without log for density."""
+ if not self._stats_computed:
+ raise ValueError(
+ "Must compute stats before log minmax no density transform"
+ )
+
+ transformed = {}
+
+ # Young's modulus - log transform then min-max normalize
+ E = properties["youngs_modulus"]
+ E_clamped = torch.clamp_min(E, 1e-8)
+ log_E = torch.log10(E_clamped)
+ transformed["youngs_modulus"] = (log_E - self.E_min) / (self.E_max - self.E_min)
+
+ # Poisson's ratio - min-max normalize directly
+ nu = properties["poissons_ratio"]
+ transformed["poissons_ratio"] = (nu - self.nu_min) / (self.nu_max - self.nu_min)
+
+ # Density - min-max normalize WITHOUT log transform
+ rho = properties["density"]
+ transformed["density"] = (rho - self.rho_min) / (self.rho_max - self.rho_min)
+
+ return transformed
+
+ def forward_transform_and_standardize(
+ self, properties: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply forward transform and standardization in one step.
+
+ Args:
+ properties: Dictionary with material properties
+
+ Returns:
+ Transformed and standardized properties
+ """
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before standardization")
+
+ if (
+ self.normalization_type == "log_minmax"
+ or self.normalization_type == "log_minmax_no_density"
+ ):
+ # For log minmax variants, forward transform already includes normalization
+ return self.forward_transform(properties)
+ else:
+ # Standard approach: forward transform + standardization
+ # First apply forward transform
+ transformed = self.forward_transform(properties)
+
+ # Then standardize each property individually
+ standardized = {}
+ standardized["youngs_modulus"] = (
+ transformed["youngs_modulus"] - self.mu[0]
+ ) / self.std[0]
+ standardized["poissons_ratio"] = (
+ transformed["poissons_ratio"] - self.mu[1]
+ ) / self.std[1]
+ standardized["density"] = (transformed["density"] - self.mu[2]) / self.std[
+ 2
+ ]
+
+ return standardized
+
+ def inverse_transform(
+ self, transformed: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply inverse transform to get back original scale.
+
+ Args:
+ transformed: Dictionary with transformed properties
+
+ Returns:
+ Properties in original scale
+ """
+ if self.normalization_type == "log_minmax":
+ return self._log_minmax_inverse_transform(transformed)
+ elif self.normalization_type == "log_minmax_no_density":
+ return self._log_minmax_no_density_inverse_transform(transformed)
+ else:
+ return self._standard_inverse_transform(transformed)
+
+ def _standard_inverse_transform(
+ self, transformed: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply standard inverse transform."""
+ original = {}
+
+ # Young's modulus - exp transform
+ original["youngs_modulus"] = torch.pow(10, transformed["youngs_modulus"])
+
+ # Poisson's ratio - inverse logit and rescale
+ p = torch.sigmoid(transformed["poissons_ratio"])
+ original["poissons_ratio"] = p * (self.nu_max - self.nu_min) + self.nu_min
+
+ # Density - exp transform
+ original["density"] = torch.pow(10, transformed["density"])
+
+ return original
+
+ def _log_minmax_inverse_transform(
+ self, transformed: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply log minmax inverse transform."""
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before inverse transform")
+
+ original = {}
+
+ # Young's modulus - inverse min-max then exp transform
+ log_E = transformed["youngs_modulus"] * (self.E_max - self.E_min) + self.E_min
+ original["youngs_modulus"] = torch.pow(10, log_E)
+
+ # Poisson's ratio - inverse min-max
+ original["poissons_ratio"] = (
+ transformed["poissons_ratio"] * (self.nu_max - self.nu_min) + self.nu_min
+ )
+
+ # Density - inverse min-max then exp transform
+ log_rho = transformed["density"] * (self.rho_max - self.rho_min) + self.rho_min
+ original["density"] = torch.pow(10, log_rho)
+
+ return original
+
+ def _log_minmax_no_density_inverse_transform(
+ self, transformed: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply log minmax inverse transform without log for density."""
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before inverse transform")
+
+ original = {}
+
+ # Young's modulus - inverse min-max then exp transform
+ log_E = transformed["youngs_modulus"] * (self.E_max - self.E_min) + self.E_min
+ original["youngs_modulus"] = torch.pow(10, log_E)
+
+ # Poisson's ratio - inverse min-max
+ original["poissons_ratio"] = (
+ transformed["poissons_ratio"] * (self.nu_max - self.nu_min) + self.nu_min
+ )
+
+ # Density - inverse min-max WITHOUT exp transform
+ original["density"] = (
+ transformed["density"] * (self.rho_max - self.rho_min) + self.rho_min
+ )
+
+ return original
+
+ def compute_stats(self, dataset_loader) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Compute statistics for normalization.
+
+ Args:
+ dataset_loader: DataLoader to compute statistics from
+
+ Returns:
+ mean and std tensors (for standard mode) or min/max tensors (for log_minmax mode)
+ """
+ if self.normalization_type == "log_minmax":
+ return self._compute_log_minmax_stats(dataset_loader)
+ elif self.normalization_type == "log_minmax_no_density":
+ return self._compute_log_minmax_no_density_stats(dataset_loader)
+ else:
+ return self._compute_standard_stats(dataset_loader)
+
+ def _compute_standard_stats(
+ self, dataset_loader
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Compute mean and std for standardization on transformed values."""
+ sum_ = torch.zeros(3)
+ sum2 = torch.zeros(3)
+ n = 0
+
+ for batch in dataset_loader:
+ if "material_properties" in batch:
+ props = batch["material_properties"]
+ # Apply forward transform
+ transformed = self._standard_transform(props)
+
+ # Stack into tensor [E, nu, rho]
+ stacked = torch.stack(
+ [
+ transformed["youngs_modulus"].flatten(),
+ transformed["poissons_ratio"].flatten(),
+ transformed["density"].flatten(),
+ ],
+ dim=-1,
+ )
+
+ sum_ += stacked.sum(0)
+ sum2 += (stacked**2).sum(0)
+ n += stacked.shape[0]
+
+ self.mu = sum_ / n
+ self.std = torch.sqrt(sum2 / n - self.mu**2)
+ self._stats_computed = True
+
+ return self.mu, self.std
+
+ def _compute_log_minmax_stats(
+ self, dataset_loader
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Compute min/max values for log minmax normalization."""
+ log_E_values = []
+ nu_values = []
+ log_rho_values = []
+
+ for batch in dataset_loader:
+ if "material_properties" in batch:
+ props = batch["material_properties"]
+
+ # Young's modulus - log transform
+ E = props["youngs_modulus"]
+ E_clamped = torch.clamp_min(E, 1e-8)
+ log_E_values.append(torch.log10(E_clamped).flatten())
+
+ # Poisson's ratio - use directly
+ nu_values.append(props["poissons_ratio"].flatten())
+
+ # Density - log transform
+ rho = props["density"]
+ rho_clamped = torch.clamp_min(rho, 1e-8)
+ log_rho_values.append(torch.log10(rho_clamped).flatten())
+
+ # Concatenate all values
+ all_log_E = torch.cat(log_E_values)
+ all_nu = torch.cat(nu_values)
+ all_log_rho = torch.cat(log_rho_values)
+
+ # Compute min/max
+ self.E_min = all_log_E.min().item()
+ self.E_max = all_log_E.max().item()
+ self.nu_min = all_nu.min().item()
+ self.nu_max = all_nu.max().item()
+ self.rho_min = all_log_rho.min().item()
+ self.rho_max = all_log_rho.max().item()
+
+ self._stats_computed = True
+
+ # Return min/max as tensors for compatibility
+ min_vals = torch.tensor([self.E_min, self.nu_min, self.rho_min])
+ max_vals = torch.tensor([self.E_max, self.nu_max, self.rho_max])
+
+ return min_vals, max_vals
+
+ def _compute_log_minmax_no_density_stats(
+ self, dataset_loader
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Compute min/max values for log minmax no density normalization."""
+ log_E_values = []
+ nu_values = []
+ rho_values = []
+
+ for batch in dataset_loader:
+ if "material_properties" in batch:
+ props = batch["material_properties"]
+
+ # Young's modulus - log transform
+ E = props["youngs_modulus"]
+ E_clamped = torch.clamp_min(E, 1e-8)
+ log_E_values.append(torch.log10(E_clamped).flatten())
+
+ # Poisson's ratio - use directly
+ nu_values.append(props["poissons_ratio"].flatten())
+
+ # Density - use directly WITHOUT log transform
+ rho_values.append(props["density"].flatten())
+
+ # Concatenate all values
+ all_log_E = torch.cat(log_E_values)
+ all_nu = torch.cat(nu_values)
+ all_rho = torch.cat(rho_values)
+
+ # Compute min/max
+ self.E_min = all_log_E.min().item()
+ self.E_max = all_log_E.max().item()
+ self.nu_min = all_nu.min().item()
+ self.nu_max = all_nu.max().item()
+ self.rho_min = all_rho.min().item()
+ self.rho_max = all_rho.max().item()
+
+ self._stats_computed = True
+
+ # Return min/max as tensors for compatibility
+ min_vals = torch.tensor([self.E_min, self.nu_min, self.rho_min])
+ max_vals = torch.tensor([self.E_max, self.nu_max, self.rho_max])
+
+ return min_vals, max_vals
+
+ def standardize(
+ self, properties: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Apply forward transform and standardization.
+
+ Args:
+ properties: Dictionary with material properties
+
+ Returns:
+ Standardized properties
+ """
+ return self.forward_transform_and_standardize(properties)
+
+ def destandardize(
+ self, standardized: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Remove standardization and apply inverse transform.
+
+ Args:
+ standardized: Dictionary with standardized properties
+
+ Returns:
+ Properties in original scale
+ """
+ return self.destandardize_and_inverse_transform(standardized)
+
+ def destandardize_and_inverse_transform(
+ self, standardized: Dict[str, torch.Tensor]
+ ) -> Dict[str, torch.Tensor]:
+ """Remove standardization and apply inverse transform in one step.
+
+ Args:
+ standardized: Dictionary with standardized properties
+
+ Returns:
+ Properties in original scale
+ """
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before destandardization")
+
+ if (
+ self.normalization_type == "log_minmax"
+ or self.normalization_type == "log_minmax_no_density"
+ ):
+ # For log minmax variants, no standardization step - direct inverse transform
+ return self.inverse_transform(standardized)
+ else:
+ # Standard approach: destandardize then inverse transform
+ # First remove standardization
+ transformed = {}
+ transformed["youngs_modulus"] = (
+ standardized["youngs_modulus"] * self.std[0] + self.mu[0]
+ )
+ transformed["poissons_ratio"] = (
+ standardized["poissons_ratio"] * self.std[1] + self.mu[1]
+ )
+ transformed["density"] = standardized["density"] * self.std[2] + self.mu[2]
+
+ # Then apply inverse transform
+ return self.inverse_transform(transformed)
+
+ def state_dict(self):
+ """Get state dict for saving."""
+ state = {
+ "nu_min": self.nu_min,
+ "nu_max": self.nu_max,
+ "normalization_type": self.normalization_type,
+ "mu": self.mu,
+ "std": self.std,
+ "_stats_computed": self._stats_computed,
+ }
+
+ if (
+ self.normalization_type == "log_minmax"
+ or self.normalization_type == "log_minmax_no_density"
+ ):
+ state.update(
+ {
+ "E_min": self.E_min,
+ "E_max": self.E_max,
+ "rho_min": self.rho_min,
+ "rho_max": self.rho_max,
+ }
+ )
+
+ return state
+
+ def load_state_dict(self, state_dict):
+ """Load from state dict."""
+ self.nu_min = state_dict["nu_min"]
+ self.nu_max = state_dict["nu_max"]
+ self.normalization_type = state_dict.get("normalization_type", "standard")
+ self.mu = state_dict.get("mu")
+ self.std = state_dict.get("std")
+ self._stats_computed = state_dict["_stats_computed"]
+
+ if (
+ self.normalization_type == "log_minmax"
+ or self.normalization_type == "log_minmax_no_density"
+ ):
+ self.E_min = state_dict.get("E_min")
+ self.E_max = state_dict.get("E_max")
+ self.rho_min = state_dict.get("rho_min")
+ self.rho_max = state_dict.get("rho_max")
+
+ def forward_transform_tensor(self, tensor: torch.Tensor) -> torch.Tensor:
+ """Apply forward transform to a tensor of shape (..., 3).
+
+ Args:
+ tensor: Tensor with last dimension [E, nu, rho]
+
+ Returns:
+ Transformed tensor
+ """
+ shape = tensor.shape
+ tensor_flat = tensor.view(-1, 3)
+
+ # Create dict from tensor
+ props = {
+ "youngs_modulus": tensor_flat[:, 0],
+ "poissons_ratio": tensor_flat[:, 1],
+ "density": tensor_flat[:, 2],
+ }
+
+ # Transform
+ transformed = self.forward_transform(props)
+
+ # Convert back to tensor
+ result = torch.stack(
+ [
+ transformed["youngs_modulus"],
+ transformed["poissons_ratio"],
+ transformed["density"],
+ ],
+ dim=-1,
+ )
+
+ return result.view(shape)
+
+ def standardize_tensor(self, tensor: torch.Tensor) -> torch.Tensor:
+ """Standardize a tensor that's already been forward transformed.
+
+ Args:
+ tensor: Tensor of shape (..., 3) with transformed values
+
+ Returns:
+ Standardized tensor
+ """
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before standardization")
+
+ if (
+ self.normalization_type == "log_minmax"
+ or self.normalization_type == "log_minmax_no_density"
+ ):
+ # For log minmax variants, already normalized in forward transform
+ return tensor
+ else:
+ # Standard approach: standardize using mu and std
+ # Ensure mu and std are on the same device as tensor
+ mu = self.mu.to(tensor.device)
+ std = self.std.to(tensor.device)
+
+ # Standardize each channel
+ standardized = (tensor - mu) / std
+ return standardized
+
+ def forward_transform_and_standardize_tensor(
+ self, tensor: torch.Tensor
+ ) -> torch.Tensor:
+ """Apply forward transform and standardization to a tensor.
+
+ Args:
+ tensor: Tensor with last dimension [E, nu, rho] in original scale
+
+ Returns:
+ Transformed and standardized tensor
+ """
+ transformed = self.forward_transform_tensor(tensor)
+ return self.standardize_tensor(transformed)
+
+ def destandardize_and_inverse_transform_tensor(
+ self, tensor: torch.Tensor
+ ) -> torch.Tensor:
+ """Remove standardization and apply inverse transform to a tensor.
+
+ Args:
+ tensor: Standardized tensor of shape (..., 3)
+
+ Returns:
+ Tensor in original scale
+ """
+ if not self._stats_computed:
+ raise ValueError("Must compute stats before destandardization")
+
+ if (
+ self.normalization_type == "log_minmax"
+ or self.normalization_type == "log_minmax_no_density"
+ ):
+ # For log minmax variants, no standardization step - direct inverse transform
+ shape = tensor.shape
+ tensor_flat = tensor.view(-1, 3)
+
+ props = {
+ "youngs_modulus": tensor_flat[:, 0],
+ "poissons_ratio": tensor_flat[:, 1],
+ "density": tensor_flat[:, 2],
+ }
+
+ original = self.inverse_transform(props)
+
+ result = torch.stack(
+ [
+ original["youngs_modulus"],
+ original["poissons_ratio"],
+ original["density"],
+ ],
+ dim=-1,
+ )
+
+ return result.view(shape)
+ else:
+ # Standard approach: destandardize then inverse transform
+ # Ensure mu and std are on the same device as tensor
+ mu = self.mu.to(tensor.device)
+ std = self.std.to(tensor.device)
+
+ # Destandardize
+ transformed = tensor * std + mu
+
+ # Apply inverse transform
+ shape = transformed.shape
+ transformed_flat = transformed.view(-1, 3)
+
+ props = {
+ "youngs_modulus": transformed_flat[:, 0],
+ "poissons_ratio": transformed_flat[:, 1],
+ "density": transformed_flat[:, 2],
+ }
+
+ original = self.inverse_transform(props)
+
+ result = torch.stack(
+ [
+ original["youngs_modulus"],
+ original["poissons_ratio"],
+ original["density"],
+ ],
+ dim=-1,
+ )
+
+ return result.view(shape)
diff --git a/deps/vomp/vomp/utils/random_utils.py b/deps/vomp/vomp/utils/random_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6297059480b04b4e751b2aeb1b6bb6b7c2938b8
--- /dev/null
+++ b/deps/vomp/vomp/utils/random_utils.py
@@ -0,0 +1,49 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+
+PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]
+
+
+def radical_inverse(base, n):
+ val = 0
+ inv_base = 1.0 / base
+ inv_base_n = inv_base
+ while n > 0:
+ digit = n % base
+ val += digit * inv_base_n
+ n //= base
+ inv_base_n *= inv_base
+ return val
+
+
+def halton_sequence(dim, n):
+ return [radical_inverse(PRIMES[dim], n) for dim in range(dim)]
+
+
+def hammersley_sequence(dim, n, num_samples):
+ return [n / num_samples] + halton_sequence(dim - 1, n)
+
+
+def sphere_hammersley_sequence(n, num_samples, offset=(0, 0), remap=False):
+ u, v = hammersley_sequence(2, n, num_samples)
+ u += offset[0] / num_samples
+ v += offset[1]
+ if remap:
+ u = 2 * u if u < 0.25 else 2 / 3 * u + 1 / 3
+ theta = np.arccos(1 - 2 * u) - np.pi / 2
+ phi = v * 2 * np.pi
+ return [phi, theta]
diff --git a/deps/vomp/vomp/utils/render_utils.py b/deps/vomp/vomp/utils/render_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..06f1a6e54b6b4ef2941912a96d8b75b4a01dd5aa
--- /dev/null
+++ b/deps/vomp/vomp/utils/render_utils.py
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import torch
+import numpy as np
+from tqdm import tqdm
+import utils3d
+from PIL import Image
+
+from ..representations import Gaussian
+from ..modules import sparse as sp
+from .random_utils import sphere_hammersley_sequence
+
+
+def yaw_pitch_r_fov_to_extrinsics_intrinsics(yaws, pitchs, rs, fovs):
+ is_list = isinstance(yaws, list)
+ if not is_list:
+ yaws = [yaws]
+ pitchs = [pitchs]
+ if not isinstance(rs, list):
+ rs = [rs] * len(yaws)
+ if not isinstance(fovs, list):
+ fovs = [fovs] * len(yaws)
+ extrinsics = []
+ intrinsics = []
+ for yaw, pitch, r, fov in zip(yaws, pitchs, rs, fovs):
+ fov = torch.deg2rad(torch.tensor(float(fov))).cuda()
+ yaw = torch.tensor(float(yaw)).cuda()
+ pitch = torch.tensor(float(pitch)).cuda()
+ orig = (
+ torch.tensor(
+ [
+ torch.sin(yaw) * torch.cos(pitch),
+ torch.cos(yaw) * torch.cos(pitch),
+ torch.sin(pitch),
+ ]
+ ).cuda()
+ * r
+ )
+ extr = utils3d.torch.extrinsics_look_at(
+ orig,
+ torch.tensor([0, 0, 0]).float().cuda(),
+ torch.tensor([0, 0, 1]).float().cuda(),
+ )
+ intr = utils3d.torch.intrinsics_from_fov_xy(fov, fov)
+ extrinsics.append(extr)
+ intrinsics.append(intr)
+ if not is_list:
+ extrinsics = extrinsics[0]
+ intrinsics = intrinsics[0]
+ return extrinsics, intrinsics
diff --git a/examples/dog.ply b/examples/dog.ply
new file mode 100644
index 0000000000000000000000000000000000000000..3e1633c14314d56760ec0070addca8d558602d5d
--- /dev/null
+++ b/examples/dog.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1808d618588f67a083bca52848070297be63d24ba3ddf1e9dae7edd75d87c69e
+size 2477313
diff --git a/examples/dozer.ply b/examples/dozer.ply
new file mode 100644
index 0000000000000000000000000000000000000000..123bce7fe994313d69468e1f1dada40bc1534c78
--- /dev/null
+++ b/examples/dozer.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b44ab835c41c4b4e2ad4b55456de1173f38e8bcef005de4d5d9d2c27f7b3749e
+size 84598795
diff --git a/examples/fiscus.ply b/examples/fiscus.ply
new file mode 100644
index 0000000000000000000000000000000000000000..b70b72e987a65ffc307d12961957bb1c2d5f9ab3
--- /dev/null
+++ b/examples/fiscus.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7516d90f1ec0764ce653819057b1b69197a56bc31683b4a7fd4fdff247437f7e
+size 74925059
diff --git a/examples/plant.ply b/examples/plant.ply
new file mode 100644
index 0000000000000000000000000000000000000000..daa7f5ca407d0b5fcd3e220e81a867a3d574aec4
--- /dev/null
+++ b/examples/plant.ply
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3a659f85e78556a90cddb2f37fcf77e9bd452fda15de6f69f6db8c5803bf9fd5
+size 12734620
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..abfc58022386b112f0a10a5b50b270a571f96dd1
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,21 @@
+gradio
+polyscope==2.5.0
+trimesh==4.8.1
+Pillow==11.0.0
+safetensors==0.6.2
+easydict==1.13
+scipy==1.14.1
+pyparsing==3.2.3
+opencv-python-headless==4.10.0.84
+numpy==1.26.4
+matplotlib==3.7.5
+torch==2.4.0
+torchvision==0.19.0
+xformers==0.0.27.post2
+spconv-cu121
+https://nvidia-kaolin.s3.us-east-2.amazonaws.com/torch-2.4.0_cu121/kaolin-0.18.0-cp312-cp312-linux_x86_64.whl
+https://huggingface.co/spaces/rishitdagli/vomp-test/resolve/main/wheels/utils3d-0.0.2-py3-none-any.whl
+https://huggingface.co/spaces/rishitdagli/vomp-test/resolve/main/wheels/diff_gaussian_rasterization-0.0.0-cp312-cp312-linux_x86_64.whl
+# VoMP: local fork with debug prints (see deps/vomp); was: git+https://github.com/nv-tlabs/vomp.git#egg=vomp
+-e ./deps/vomp
+https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.0.post2/flash_attn-2.7.0.post2+cu12torch2.4cxx11abiFALSE-cp312-cp312-linux_x86_64.whl
\ No newline at end of file
diff --git a/wheels/diff_gaussian_rasterization-0.0.0-cp312-cp312-linux_x86_64.whl b/wheels/diff_gaussian_rasterization-0.0.0-cp312-cp312-linux_x86_64.whl
new file mode 100644
index 0000000000000000000000000000000000000000..625f327f392aebab73bbeec6e5f2e34f174a5a90
--- /dev/null
+++ b/wheels/diff_gaussian_rasterization-0.0.0-cp312-cp312-linux_x86_64.whl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:099693acb4014a7c68a0e973dbbb401e1555756f0a3471112bfc958a0acdce30
+size 666144
diff --git a/wheels/utils3d-0.0.2-py3-none-any.whl b/wheels/utils3d-0.0.2-py3-none-any.whl
new file mode 100644
index 0000000000000000000000000000000000000000..7c201d6373c2e4369def63da4a353fec16083957
--- /dev/null
+++ b/wheels/utils3d-0.0.2-py3-none-any.whl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4caaf6a3e6ddca59d824d1436f6a6139d7410d61a5d33af5c5e57e041d1d21c2
+size 88958