diff --git a/.github/ISSUE_TEMPLATE/material-correction.yml b/.github/ISSUE_TEMPLATE/material-correction.yml new file mode 100644 index 0000000..554a2bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/material-correction.yml @@ -0,0 +1,44 @@ +name: Material data correction +description: Report an incorrect property value or missing data +labels: ["data-correction"] +body: + - type: input + id: material + attributes: + label: Material + description: Which material has the issue + placeholder: e.g. stainless.s316L, aluminum.a6061 + validations: + required: true + + - type: input + id: property + attributes: + label: Property + description: Which property is wrong or missing + placeholder: e.g. density, yield_strength, thermal_conductivity + validations: + required: true + + - type: input + id: current_value + attributes: + label: Current value (if wrong) + placeholder: e.g. 8.0 g/cm³ + + - type: input + id: correct_value + attributes: + label: Correct value + placeholder: e.g. 7.99 g/cm³ + validations: + required: true + + - type: input + id: source + attributes: + label: Source / reference + description: Where does the correct value come from? + placeholder: e.g. ASM Handbook Vol. 2, MatWeb, manufacturer datasheet URL + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/material-request.yml b/.github/ISSUE_TEMPLATE/material-request.yml new file mode 100644 index 0000000..e92a6cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/material-request.yml @@ -0,0 +1,65 @@ +name: Material request +description: Request a new material to be added to the library +labels: ["material-request"] +body: + - type: input + id: material_name + attributes: + label: Material name + description: Common name or trade name + placeholder: e.g. Inconel 718, Sapphire, Carbon Fiber T300 + validations: + required: true + + - type: dropdown + id: category + attributes: + label: Category + options: + - Metal / Alloy + - Ceramic + - Plastic / Polymer + - Scintillator / Detector + - Glass + - Composite + - Electronics + - Liquid + - Gas + - Other + validations: + required: true + + - type: textarea + id: properties + attributes: + label: Known properties + description: | + Any values you already know. Don't worry about completeness — + we'll fill in the rest from datasheets / literature. + placeholder: | + Density: 8.19 g/cm³ + Yield strength: 1034 MPa + Melting point: 1260 °C + Composition: Ni 52%, Cr 19%, Fe 18%, Nb 5%, Mo 3% + + - type: textarea + id: use_case + attributes: + label: Use case + description: What are you using this material for? Helps us prioritize. + placeholder: e.g. PET scanner shielding, 3D printed bracket, Geant4 simulation + + - type: input + id: datasheet_url + attributes: + label: Datasheet or reference URL (optional) + placeholder: https://www.matweb.com/... + + - type: textarea + id: appearance + attributes: + label: Visual appearance (optional) + description: | + If you need PBR textures, describe the finish. We'll try to + match from mat-vis's texture library. + placeholder: e.g. brushed finish, polished mirror, matte black anodized diff --git a/.github/workflows/catalog.yml b/.github/workflows/catalog.yml new file mode 100644 index 0000000..bed83fc --- /dev/null +++ b/.github/workflows/catalog.yml @@ -0,0 +1,30 @@ +name: Update catalog + +on: + push: + branches: [dev] + paths: ["src/pymat/data/**"] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + catalog: + name: Regenerate material catalog + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -e '.[dev]' + - run: python scripts/generate_catalog.py --skip-thumbnails + - uses: peter-evans/create-pull-request@v7 + with: + title: "docs: regenerate material catalog" + commit-message: "docs: regenerate catalog from TOML data" + branch: docs/catalog-update + add-paths: docs/catalog/ + delete-branch: true diff --git a/.github/workflows/enrich-vis.yml b/.github/workflows/enrich-vis.yml new file mode 100644 index 0000000..2b3cd12 --- /dev/null +++ b/.github/workflows/enrich-vis.yml @@ -0,0 +1,45 @@ +name: Propose vis mappings + +on: + repository_dispatch: + types: [mat-vis-release] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + enrich: + name: Auto-propose vis mappings + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -e '.[dev]' + - run: python scripts/enrich_vis.py -o /tmp/proposed_vis.toml + - name: Check for proposals + id: check + run: | + if [ -s /tmp/proposed_vis.toml ]; then + echo "has_proposals=true" >> "$GITHUB_OUTPUT" + echo "Proposed vis mappings:" + cat /tmp/proposed_vis.toml + else + echo "has_proposals=false" >> "$GITHUB_OUTPUT" + echo "No new mappings to propose" + fi + - uses: peter-evans/create-pull-request@v7 + if: steps.check.outputs.has_proposals == 'true' + with: + title: "chore: update vis mappings from mat-vis" + commit-message: "chore: auto-proposed vis mappings" + branch: chore/enrich-vis + body: | + Auto-generated vis mapping proposals from mat-vis index. + Review each material's suggested appearance match before merging. + + Proposed mappings are in the commit diff. + delete-branch: true diff --git a/.gitignore b/.gitignore index 880c208..d523312 100644 --- a/.gitignore +++ b/.gitignore @@ -223,3 +223,4 @@ justfile.local # Cursor local config .cursor/ +tests/visual_output/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4eb3a2a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Contributing to mat (py-materials) + +## Ways to contribute + +### Request a material + +Don't have the data? [Open a material request][request] — tell us +what you need and why. We'll source the properties from literature +and datasheets. + +### Add a material + +Have the data? Open a PR adding a TOML entry. Here's the template: + +```toml +# src/pymat/data/.toml +# Add under the appropriate category file (metals.toml, plastics.toml, etc.) + +[my_material] +name = "My Material Name" +formula = "Fe3O4" # optional — chemical formula +composition = {Fe = 0.72, O = 0.28} # optional — element mass fractions + +[my_material.mechanical] +density_value = 5.17 +density_unit = "g/cm^3" +youngs_modulus_value = 200 # optional +youngs_modulus_unit = "GPa" + +[my_material.thermal] +melting_point_value = 1597 # optional +melting_point_unit = "degC" + +[my_material.pbr] +base_color = [0.1, 0.1, 0.1, 1.0] # RGBA, 0-1 +metallic = 0.8 # 0 = dielectric, 1 = metal +roughness = 0.5 # 0 = glossy, 1 = rough + +# Visual appearance mapping (optional — mat-vis must have matching textures) +[my_material.vis] +default = "natural" + +[my_material.vis.finishes] +natural = "ambientcg/Metal_SomeID" # mat-vis source ID +``` + +#### What's required + +- `name` — human-readable +- `density` — in `g/cm³` (needed for `compute_mass()`) +- `base_color`, `metallic`, `roughness` — for rendering + +Everything else is optional. More data is better, but partial +entries are welcome — we can enrich later. + +#### Hierarchy + +Materials support parent → child hierarchy: + +```toml +[aluminum] +name = "Aluminum" +# base properties... + +[aluminum.a6061] +name = "Aluminum 6061-T6" +grade = "6061" +temper = "T6" +# only overrides — inherits everything from parent +``` + +### Fix a value + +Spotted an error? [Open a correction issue][correction] with the +correct value and a source citation. Or open a PR directly — +edit the TOML, cite your source in the commit message. + +### Improve the code + +See the [open issues][issues] for bugs, features, and +refactoring tasks. The standard workflow: + +1. Fork the repo +2. Create a branch from `dev` +3. Make your changes +4. Run tests: `pytest tests/ -v` +5. Run linter: `ruff check src/ tests/` +6. Open a PR against `dev` + +## Data quality + +- **Cite your sources.** Every property value should be traceable + to a datasheet, handbook, or paper. Include the reference in the + commit message or PR description. +- **Use SI-compatible units** with the `_value` / `_unit` suffix + pattern (e.g. `density_value = 8.0`, `density_unit = "g/cm^3"`). +- **Don't fabricate values.** If a property isn't known, leave it + out. `None` is better than a guess. +- **PBR values can be approximate.** Rendering properties are + visual, not physical measurements. Matching "looks like steel" + is fine. + +## Visual appearance (vis) + +Materials can optionally link to [mat-vis][mat-vis] textures for +PBR rendering. To find matching textures: + +```python +from pymat import vis +vis.search(category="metal", roughness=0.3) +``` + +Add the match to the `[vis]` section of the TOML. If you're +unsure, leave it out — the `enrich_vis.py` script proposes +matches automatically. + +[request]: https://github.com/MorePET/mat/issues/new?template=material-request.yml +[correction]: https://github.com/MorePET/mat/issues/new?template=material-correction.yml +[issues]: https://github.com/MorePET/mat/issues +[mat-vis]: https://github.com/MorePET/mat-vis diff --git a/README.md b/README.md index 4368fbe..72cdbfd 100644 --- a/README.md +++ b/README.md @@ -1,338 +1,458 @@ -# mat +# pymat -Material database for CAD and Monte Carlo particle transport. - -Shared TOML data files with language-specific packages: - -| Package | Language | Registry | Import | -|---------|----------|----------|--------| -| [py-materials](https://pypi.org/project/py-materials/) | Python | PyPI | `import pymat` | -| [rs-materials](mat-rs/) | Rust | crates.io | `use rs_materials::...` | +A hierarchical material library for CAD applications and Monte Carlo particle transport, with build123d integration. ## Features -### Python (py-materials) - - **Hierarchical Materials**: Chain grades, tempers, treatments, and vendors - **Property Inheritance**: Children inherit parent properties unless overridden - **Lazy Loading**: Categories load on first access +- **TOML Data Storage**: Easy-to-edit material definitions +- **Formula Parsing + Molar Mass**: Computed from the chemical formula via `Material.molar_mass`, with fractional stoichiometry (`Lu1.8Y0.2SiO5`) and dopant suffix stripping (`LYSO:Ce`). Atomic weights mirror the Rust `rs-materials` crate for Python ↔ Rust parity. - **build123d Integration**: Apply materials to shapes with automatic mass calculation - **PBR Rendering**: Physically-based rendering properties for visualization -- **periodictable Integration**: Auto-fill density from chemical formulas +- **periodictable Integration**: Auto-fill composition from chemical formulas for compounds; auto-fill density for pure elements - **Factory Functions**: Temperature/pressure-dependent materials (water, air, saline) - -### Rust (rs-materials) - -- **TOML Database Reader**: Loads all 7 material categories with property inheritance -- **Formula Parser**: Fractional stoichiometry (`Lu1.8Y0.2SiO5` → element/count pairs) -- **Mass/Atom Fraction Conversion**: Bidirectional with roundtrip correctness -- **Scintillator Properties**: Light yield, decay time, emission peak, refractive index -- **Thread-Safe**: `Send + Sync` for `Arc` sharing in multi-threaded transport engines +- **Separation of Concerns**: Optical properties (physics) separate from PBR (visualization) +- **Python 3.10 – 3.13 supported**, core library depends only on `pint` ## Installation -### Python - ```bash -# With uv (recommended) -uv add git+https://github.com/MorePET/mat.git@latest - -# Or specify version -uv add git+https://github.com/MorePET/mat.git@v1.0.0 +pip install py-materials +# or: uv add py-materials ``` -### Rust - -```toml -[dependencies] -rs-materials = { git = "https://github.com/MorePET/mat.git" } -``` +Core install includes `pint` + `periodictable`. No extras needed — +PBR textures are fetched on demand from [mat-vis][mat-vis] via +pure-Python HTTP range reads (zero binary deps). ## Quick Start ## Creating Materials - Create materials with convenient parameters: +Create materials with convenient parameters: ```python from pymat import Material - # Using convenience parameters - steel = Material(name="Steel", density=7.85) - # With visualization color - aluminum = Material(name="Aluminum", density=2.7, color=(0.88, 0.88, 0.88)) - # With formula - lyso = Material(name="LYSO", formula="Lu1.8Y0.2SiO5", density=7.1) - assert lyso.formula == "Lu1.8Y0.2SiO5" +# Using convenience parameters +steel = Material(name="Steel", density=7.85) +assert steel.density == 7.85 + +# With visualization color +aluminum = Material(name="Aluminum", density=2.7, color=(0.88, 0.88, 0.88)) +assert aluminum.properties.pbr.base_color[:3] == (0.88, 0.88, 0.88) + +# With formula +lyso = Material(name="LYSO", formula="Lu1.8Y0.2SiO5", density=7.1) +assert lyso.formula == "Lu1.8Y0.2SiO5" ``` ## Using Property Groups - Define multiple properties at once using property group dictionaries: +Define multiple properties at once using property group dictionaries: ```python from pymat import Material - # Define steel with multiple property groups - steel = Material( - name="Stainless Steel 304", - mechanical={"density": 8.0, "youngs_modulus": 193, "yield_strength": 170}, - thermal={"melting_point": 1450, "thermal_conductivity": 15.1}, - pbr={"base_color": (0.75, 0.75, 0.77, 1.0), "metallic": 1.0} - ) assert steel.properties.pbr.metallic == 1.0 +# Define steel with multiple property groups +steel = Material( + name="Stainless Steel 304", + mechanical={"density": 8.0, "youngs_modulus": 193, "yield_strength": 170}, + thermal={"melting_point": 1450, "thermal_conductivity": 15.1}, + pbr={"base_color": (0.75, 0.75, 0.77, 1.0), "metallic": 1.0}, +) + +assert steel.properties.mechanical.density == 8.0 +assert steel.properties.mechanical.youngs_modulus == 193 +assert steel.properties.thermal.melting_point == 1450 +assert steel.properties.pbr.metallic == 1.0 ``` ## Applying Materials to Shapes - Apply materials to build123d shapes for visualization and mass calculation: +Apply materials to build123d shapes for visualization and mass calculation: ```python from build123d import Box - from pymat import Material - # Create material - steel = Material(name="Steel", density=7.85, color=(0.7, 0.7, 0.7)) +from pymat import Material + +# Create material +steel = Material(name="Steel", density=7.85, color=(0.7, 0.7, 0.7)) + +# Create shape and apply material +box = Box(10, 10, 10) +steel.apply_to(box) + +assert box.material.name == "Steel" +assert box.mass > 0 +assert box.color is not None +``` + +## Computing Molar Mass + +`Material.molar_mass` is a computed property that parses the +chemical formula and looks up each element's atomic weight. +It supports fractional stoichiometry and strips dopant +notation like `LYSO:Ce` so doped-crystal aliases work +unchanged. + +Nothing is stored — it recomputes on each access. That's +intentional: molar mass is definitionally derived from +`formula` and should never drift. Missing or unknown-element +formulas return `None`. See +`docs/decisions/0001-derived-chemistry-properties-live-on-material.md`. + +```python +from pymat import Material + +# Pure element +iron = Material(name="Iron", formula="Fe") +assert iron.molar_mass == 55.85 + +# Simple compound +alumina = Material(name="Alumina", formula="Al2O3") +assert abs(alumina.molar_mass - 101.96) < 0.01 + +# Fractional stoichiometry (a PET-scanner scintillator) +lyso = Material(name="LYSO", formula="Lu1.8Y0.2SiO5") +assert abs(lyso.molar_mass - 440.87) < 0.1 + +# Dopant suffix is stripped +lyso_ce = Material(name="LYSO:Ce", formula="Lu1.8Y0.2SiO5:Ce") +assert lyso_ce.molar_mass == lyso.molar_mass + +# Unit-aware companion accessor (Pint Quantity) +qty = iron.molar_mass_qty +assert qty.to("kg/mol").magnitude == pytest.approx(0.05585, abs=1e-4) + +# Gracefully returns None when no formula is set +unknown = Material(name="Unknown Alloy") +assert unknown.molar_mass is None +``` + +## Low-level: `pymat.elements` + +For callers that don't need a full `Material` object — +e.g. quick stoichiometry calculations inside a Monte Carlo +transport loop — the low-level `pymat.elements` module +exposes the same machinery directly. + +The `ATOMIC_WEIGHT` table is a line-for-line mirror of the +Rust `rs-materials` crate, so Python and Rust Monte Carlo +engines get identical molar masses byte-for-byte. + +```python +from pymat.elements import ( + ATOMIC_WEIGHT, + compute_molar_mass, + parse_formula, +) + +# Atomic weight lookup +assert ATOMIC_WEIGHT["Fe"] == 55.85 +assert ATOMIC_WEIGHT["Lu"] == 175.0 + +# Formula parser: fractional stoichiometry + repeat handling +counts = parse_formula("Lu1.8Y0.2SiO5") +assert counts == {"Lu": 1.8, "Y": 0.2, "Si": 1.0, "O": 5.0} - # Create shape and apply material - box = Box(10, 10, 10) - steel.apply_to(box) assert box.color is not None +# Molar mass directly from a formula string +assert abs(compute_molar_mass("Al2O3") - 101.96) < 0.01 ``` ## Chainable Material Hierarchy - Build hierarchies with grades, tempers, and treatments: +Build hierarchies with grades, tempers, and treatments: ```python from pymat import Material - # Create base stainless steel - stainless = Material( - name="Stainless Steel", - density=8.0, - thermal={"melting_point": 1450} - ) - - # Add grade - s304 = stainless.grade_("304", name="SS 304", mechanical={"yield_strength": 170}) - # Add treatment - passivated = s304.treatment_("passivated", name="SS 304 Passivated") assert passivated.density == 8.0 # Inherited through chain +# Create base stainless steel +stainless = Material(name="Stainless Steel", density=8.0, thermal={"melting_point": 1450}) + +# Add grade +s304 = stainless.grade_("304", name="SS 304", mechanical={"yield_strength": 170}) +assert s304.density == 8.0 # Inherited +assert s304.properties.mechanical.yield_strength == 170 + +# Add treatment +passivated = s304.treatment_("passivated", name="SS 304 Passivated") +assert ( + passivated.path == "stainless_steel.304.passivated" +) # name -> lowercase with underscores +assert passivated.density == 8.0 # Inherited through chain ``` ## Direct Material Access - Load materials and access them directly from the library: +Load materials and access them directly from the library: ```python -from pymat import stainless, aluminum, lyso +from pymat import aluminum, lyso, stainless + +# Direct access to materials +s316L = stainless.s316L +assert s316L.grade == "316L" - # Direct access to materials - s316L = stainless.s316L - al6061 = aluminum.a6061 - lyso_crystal = lyso - assert "LYSO" in lyso_crystal.name +al6061 = aluminum.a6061 +assert al6061.density == 2.7 # Inherited from aluminum + +lyso_crystal = lyso +assert "LYSO" in lyso_crystal.name ``` ## Physics Properties vs Visualization - Understand the difference between measured optical properties (physics) - and rendering properties (visualization): +Understand the difference between measured optical properties (physics) +and rendering properties (visualization): ```python from pymat import Material - # Create transparent material - glass = Material( - name="Optical Glass", - color=(0.9, 0.9, 0.9, 0.8), # Visual: 80% opaque white - optical={"transparency": 95, "refractive_index": 1.517}, # Physics: 95% transmission - pbr={"transmission": 0.8} # Rendering: how transparent it looks - ) +# Create transparent material +glass = Material( + name="Optical Glass", + color=(0.9, 0.9, 0.9, 0.8), # Visual: 80% opaque white + optical={"transparency": 95, "refractive_index": 1.517}, # Physics: 95% transmission + pbr={"transmission": 0.8}, # Rendering: how transparent it looks +) + +# Physics properties (measured) +assert glass.properties.optical.transparency == 95 +assert glass.properties.optical.refractive_index == 1.517 - # Physics properties (measured) - # Visualization properties (rendering) assert glass.properties.pbr.transmission == 0.8 +# Visualization properties (rendering) +assert glass.properties.pbr.base_color[3] == 0.8 # Alpha +assert glass.properties.pbr.transmission == 0.8 ``` ## Scintillator-Specific Properties - Define detector crystals with optical physics properties: +Define detector crystals with optical physics properties: ```python from pymat import Material - lyso_crystal = Material( - name="LYSO:Ce Crystal", - density=7.1, - optical={ - "refractive_index": 1.82, - "transparency": 92, - "light_yield": 32000, # photons/MeV - "decay_time": 41, # ns - "emission_peak": 420, # nm - }, - pbr={"base_color": (0.0, 1.0, 1.0, 0.85), "transmission": 0.85} - ) assert lyso_crystal.properties.pbr.transmission == 0.85 +lyso_crystal = Material( + name="LYSO:Ce Crystal", + density=7.1, + optical={ + "refractive_index": 1.82, + "transparency": 92, + "light_yield": 32000, # photons/MeV + "decay_time": 41, # ns + "emission_peak": 420, # nm + }, + pbr={"base_color": (0.0, 1.0, 1.0, 0.85), "transmission": 0.85}, +) + +assert lyso_crystal.properties.optical.light_yield == 32000 +assert lyso_crystal.properties.optical.decay_time == 41 +assert lyso_crystal.properties.pbr.transmission == 0.85 ``` ## Temperature-Dependent Materials - Use factory functions for materials with properties that depend on external parameters: +Use factory functions for materials with properties that depend on external parameters: ```python from pymat.factories import water - # Water at different temperatures - cold_water = water(4) # Max density - room_water = water(20) # Room temperature - hot_water = water(80) # Heated - # Verify realistic values assert 0.95 < hot_water.density < 0.98 +# Water at different temperatures +cold_water = water(4) # Max density +room_water = water(20) # Room temperature +hot_water = water(80) # Heated + +assert cold_water.density > room_water.density +assert room_water.density > hot_water.density + +# Verify realistic values +assert 0.99 < cold_water.density < 1.01 +assert 0.95 < hot_water.density < 0.98 ``` ## Air at Different Conditions - Create air material at specific temperature and pressure: +Create air material at specific temperature and pressure: ```python from pymat.factories import air - sea_level = air(15, 1.0) # 15°C, 1 atm - high_altitude = air(15, 0.5) # 15°C, 0.5 atm (5500m) +sea_level = air(15, 1.0) # 15°C, 1 atm +high_altitude = air(15, 0.5) # 15°C, 0.5 atm (5500m) - assert sea_level.density > high_altitude.density +assert sea_level.density > high_altitude.density ``` ## Saline Solutions - Create solutions with specific concentration and temperature: +Create solutions with specific concentration and temperature: ```python from pymat.factories import saline, water - # Physiological saline at body temperature - phantom = saline(0.9, temperature_c=37) - # Saline is slightly denser than pure water at same temperature - pure_water_37 = water(37) - # Seawater (3.5% NaCl) at 20°C - seawater = saline(3.5, temperature_c=20) - # Higher concentration = higher density - assert seawater.density > phantom.density +# Physiological saline at body temperature +phantom = saline(0.9, temperature_c=37) +# Saline is slightly denser than pure water at same temperature +pure_water_37 = water(37) +assert phantom.density > pure_water_37.density + +# Seawater (3.5% NaCl) at 20°C +seawater = saline(3.5, temperature_c=20) +# Higher concentration = higher density +assert seawater.density > phantom.density ``` ## Loading Metal Materials - Access various metal materials from the metals category: +Access various metal materials from the metals category: ```python -from pymat import stainless, aluminum, copper - - # Stainless steel variants - s304 = stainless.s304 - s316L = stainless.s316L - # Aluminum alloys - al6061 = aluminum.a6061 - al7075 = aluminum.a7075 - # Copper - copper_material = copper - assert copper_material.density == 8.96 +from pymat import aluminum, copper, stainless + +# Stainless steel variants +s304 = stainless.s304 +s316L = stainless.s316L +assert s304.density == s316L.density # Same base density + +# Aluminum alloys +al6061 = aluminum.a6061 +_ = aluminum.a7075 +assert al6061.density == 2.7 + +# Copper +copper_material = copper +assert copper_material.density == 8.96 ``` ## Plastic Materials - Access plastic materials for 3D printing and engineering: +Access plastic materials for 3D printing and engineering: ```python -from pymat import peek, pla, pc, pmma +from pymat import pc, peek, pla, pmma - # Engineering plastics - # 3D printing plastics - # Transparent plastics assert pc.properties.optical.transparency == 89 +# Engineering plastics +assert peek.properties.manufacturing.print_nozzle_temp == 360 + +# 3D printing plastics +assert pla.properties.manufacturing.printable_fdm is True + +# Transparent plastics +assert pmma.properties.optical.transparency == 92 +assert pc.properties.optical.transparency == 89 ``` ## Scintillator Crystals - Access scintillator materials for radiation detectors: +Access scintillator materials for radiation detectors: ```python -from pymat import lyso, bgo, nai +from pymat import bgo, lyso, nai + +# LYSO crystal +assert lyso.properties.optical.light_yield == 32000 +assert lyso.properties.optical.refractive_index == 1.82 + +# BGO crystal +assert bgo.properties.optical.light_yield == 8500 - # LYSO crystal - # BGO crystal - # NaI crystal - assert nai.properties.optical.light_yield == 38000 +# NaI crystal +assert nai.properties.optical.light_yield == 38000 ``` ## Gas Materials - Access gases for simulation and detector design: +Access gases for simulation and detector design: ```python -from pymat import air, nitrogen, argon, helium, xenon +from pymat import air, argon, helium, nitrogen, xenon - # Common gases at STP - # Detector gases - assert argon.properties.compliance.radiation_resistant == True +# Common gases at STP +assert 0.0012 < air.density < 0.0013 # g/cm³ +assert nitrogen.density > helium.density # Helium is lightest +assert xenon.density > argon.density # Heavier noble gases + +# Detector gases +assert argon.properties.compliance.radiation_resistant is True ``` ## Property Inheritance - Child materials inherit properties from parents unless overridden: +Child materials inherit properties from parents unless overridden: ```python from pymat import Material - # Create material hierarchy - root = Material( - name="Base", - density=7.8, - thermal={"melting_point": 1500, "thermal_conductivity": 50} - ) - - grade1 = root.grade_("G1", mechanical={"yield_strength": 400}) - # Override inherited property - grade2 = root.grade_("G2", thermal={"melting_point": 1600}) - assert grade2.properties.thermal.melting_point == 1600 # Overridden +# Create material hierarchy +root = Material( + name="Base", density=7.8, thermal={"melting_point": 1500, "thermal_conductivity": 50} +) + +grade1 = root.grade_("G1", mechanical={"yield_strength": 400}) +assert grade1.density == 7.8 # Inherited +assert grade1.properties.mechanical.yield_strength == 400 # New property +assert grade1.properties.thermal.melting_point == 1500 # Inherited + +# Override inherited property +grade2 = root.grade_("G2", thermal={"melting_point": 1600}) +assert grade2.properties.thermal.melting_point == 1600 # Overridden ``` ## Automatic Mass Calculation - Materials with density automatically calculate shape mass: +Materials with density automatically calculate shape mass: ```python from build123d import Box - from pymat import stainless, aluminum - # 10x10x10 mm³ box = 1000 mm³ = 1 cm³ - steel_box = Box(10, 10, 10) - stainless.apply_to(steel_box) +from pymat import aluminum, stainless + +# 10x10x10 mm³ box = 1000 mm³ = 1 cm³ +steel_box = Box(10, 10, 10) +stainless.apply_to(steel_box) - # Density = 8.0 g/cm³, Volume = 1 cm³ → Mass = 8.0 g - # Aluminum box - al_box = Box(10, 10, 10) - aluminum.apply_to(al_box) +# Density = 8.0 g/cm³, Volume = 1 cm³ → Mass = 8.0 g +assert 7.9 < steel_box.mass < 8.1 - # Density = 2.7 g/cm³ → Mass = 2.7 g - assert 2.6 < al_box.mass < 2.8 +# Aluminum box +al_box = Box(10, 10, 10) +aluminum.apply_to(al_box) + +# Density = 2.7 g/cm³ → Mass = 2.7 g +assert 2.6 < al_box.mass < 2.8 ``` ## Material Visualization - Materials render with appropriate colors for visualization: +Materials render with appropriate colors for visualization: ```python from build123d import Box - from pymat import stainless, aluminum, lyso - # Create shapes - steel_part = Box(10, 10, 10) - al_part = Box(10, 10, 10) - crystal = Box(10, 10, 10) +from pymat import aluminum, lyso, stainless + +# Create shapes +steel_part = Box(10, 10, 10) +al_part = Box(10, 10, 10) +crystal = Box(10, 10, 10) + +# Apply materials +stainless.apply_to(steel_part) +aluminum.apply_to(al_part) +lyso.apply_to(crystal) - # Apply materials - stainless.apply_to(steel_part) - aluminum.apply_to(al_part) - lyso.apply_to(crystal) +# Verify colors are set +assert steel_part.color is not None +assert al_part.color is not None +assert crystal.color is not None - # Verify colors are set - # Colors should differ assert crystal.color != steel_part.color +# Colors should differ +assert steel_part.color != al_part.color +assert crystal.color != steel_part.color ``` ## Material Categories @@ -393,12 +513,28 @@ my_material = materials["my_material"] ### Enrichment from Chemical Formulas -```python -from pymat import enrich_from_periodictable +`enrich_from_periodictable` reads the material's `formula`, populates +the `composition` dict (element → atom count), and sets the density +**only for pure elements** — compound density is not derivable from +`periodictable`'s dataset and requires a crystallographic source like +Materials Project. Molar mass is always available regardless, via the +computed `Material.molar_mass` property (see the Quick Start). -material = Material(name="Aluminum Oxide", formula="Al2O3") -enrich_from_periodictable(material) -print(material.density) # ~3.95 g/cm³ +```python +from pymat import Material, enrich_from_periodictable + +# Pure element — density is set +iron = Material(name="Iron", formula="Fe") +enrich_from_periodictable(iron) +assert iron.density == 7.874 # set from periodictable +assert iron.molar_mass == 55.85 # computed from formula + +# Compound — composition is set, density is not +alumina = Material(name="Alumina", formula="Al2O3") +enrich_from_periodictable(alumina) +assert alumina.composition == {"Al": 2, "O": 3} +assert alumina.molar_mass == 101.96 # computed from formula +assert alumina.density is None # use enrich_from_matproj for compounds ``` ## Optical vs PBR Properties @@ -418,11 +554,40 @@ print(material.density) # ~3.95 g/cm³ These can differ intentionally! A material might be physically transparent (95% optical transmission) but rendered opaque (0% pbr transmission) for CAD clarity. +## Material Catalog + +Browse all materials with properties and thumbnails: +[**docs/catalog/**](docs/catalog/) + +Generated from TOML data + [mat-vis][mat-vis] textures via +`python scripts/generate_catalog.py`. + +## Contributing + +[**CONTRIBUTING.md**](CONTRIBUTING.md) — how to request, add, or +correct materials. Issue templates for +[material requests](https://github.com/MorePET/mat/issues/new?template=material-request.yml) +and [data corrections](https://github.com/MorePET/mat/issues/new?template=material-correction.yml). + ## License MIT +## Design Decisions (ADRs) + +Non-trivial architectural decisions live under `docs/decisions/` as +lightweight ADRs. They explain why the code is shaped the way it is +and the conditions under which the decision should be revisited. + +- [ADR-0001 — Derived chemistry properties live on `Material`](docs/decisions/0001-derived-chemistry-properties-live-on-material.md) + (why `molar_mass` is a computed `@property`, not a stored field) + ## Links - **GitHub**: https://github.com/MorePET/mat - **Issues**: https://github.com/MorePET/mat/issues +- **PyPI**: https://pypi.org/project/py-materials/ +- **Rust crate** (`rs-materials`): https://crates.io/crates/rs-materials +- **PBR textures** (`mat-vis`): https://github.com/MorePET/mat-vis + +[mat-vis]: https://github.com/MorePET/mat-vis diff --git a/docs/README_TEMPLATE.md b/docs/README_TEMPLATE.md index def1e16..d0d92d1 100644 --- a/docs/README_TEMPLATE.md +++ b/docs/README_TEMPLATE.md @@ -1,6 +1,6 @@ # pymat -A hierarchical material library for CAD applications with build123d integration. +A hierarchical material library for CAD applications and Monte Carlo particle transport, with build123d integration. ## Features @@ -8,20 +8,28 @@ A hierarchical material library for CAD applications with build123d integration. - **Property Inheritance**: Children inherit parent properties unless overridden - **Lazy Loading**: Categories load on first access - **TOML Data Storage**: Easy-to-edit material definitions +- **Formula Parsing + Molar Mass**: Computed from the chemical formula via `Material.molar_mass`, with fractional stoichiometry (`Lu1.8Y0.2SiO5`) and dopant suffix stripping (`LYSO:Ce`). Atomic weights mirror the Rust `rs-materials` crate for Python ↔ Rust parity. - **build123d Integration**: Apply materials to shapes with automatic mass calculation - **PBR Rendering**: Physically-based rendering properties for visualization -- **periodictable Integration**: Auto-fill density from chemical formulas +- **periodictable Integration**: Auto-fill composition from chemical formulas for compounds; auto-fill density for pure elements - **Factory Functions**: Temperature/pressure-dependent materials (water, air, saline) - **Separation of Concerns**: Optical properties (physics) separate from PBR (visualization) +- **Python 3.10 – 3.13 supported**, core library depends only on `pint` ## Installation ```bash -# With uv (recommended) -uv add git+https://github.com/MorePET/py-mat.git@latest +# From PyPI (recommended) +pip install py-materials +# or: uv add py-materials -# Or specify version -uv add git+https://github.com/MorePET/py-mat.git@v1.0.0 +# From the main branch (development) +pip install git+https://github.com/MorePET/mat.git@main + +# With optional extras +pip install "py-materials[periodictable]" # auto-fill from chemical formulas +pip install "py-materials[build123d]" # build123d Shape integration (Python <= 3.12) +pip install "py-materials[all]" # everything above ``` ## Quick Start @@ -86,12 +94,28 @@ my_material = materials["my_material"] ### Enrichment from Chemical Formulas -```python -from pymat import enrich_from_periodictable +`enrich_from_periodictable` reads the material's `formula`, populates +the `composition` dict (element → atom count), and sets the density +**only for pure elements** — compound density is not derivable from +`periodictable`'s dataset and requires a crystallographic source like +Materials Project. Molar mass is always available regardless, via the +computed `Material.molar_mass` property (see the Quick Start). -material = Material(name="Aluminum Oxide", formula="Al2O3") -enrich_from_periodictable(material) -print(material.density) # ~3.95 g/cm³ +```python +from pymat import Material, enrich_from_periodictable + +# Pure element — density is set +iron = Material(name="Iron", formula="Fe") +enrich_from_periodictable(iron) +assert iron.density == 7.874 # set from periodictable +assert iron.molar_mass == 55.85 # computed from formula + +# Compound — composition is set, density is not +alumina = Material(name="Alumina", formula="Al2O3") +enrich_from_periodictable(alumina) +assert alumina.composition == {"Al": 2, "O": 3} +assert alumina.molar_mass == 101.96 # computed from formula +assert alumina.density is None # use enrich_from_matproj for compounds ``` ## Optical vs PBR Properties @@ -115,7 +139,18 @@ These can differ intentionally! A material might be physically transparent (95% MIT +## Design Decisions (ADRs) + +Non-trivial architectural decisions live under `docs/decisions/` as +lightweight ADRs. They explain why the code is shaped the way it is +and the conditions under which the decision should be revisited. + +- [ADR-0001 — Derived chemistry properties live on `Material`](docs/decisions/0001-derived-chemistry-properties-live-on-material.md) + (why `molar_mass` is a computed `@property`, not a stored field) + ## Links -- **GitHub**: https://github.com/MorePET/py-mat -- **Issues**: https://github.com/MorePET/py-mat/issues +- **GitHub**: https://github.com/MorePET/mat +- **Issues**: https://github.com/MorePET/mat/issues +- **PyPI**: https://pypi.org/project/py-materials/ +- **Rust crate** (`rs-materials`): https://crates.io/crates/rs-materials diff --git a/docs/catalog/README.md b/docs/catalog/README.md new file mode 100644 index 0000000..d5862ae --- /dev/null +++ b/docs/catalog/README.md @@ -0,0 +1,13 @@ +# Material Catalog + +Auto-generated from py-mat TOML data + mat-vis textures. + +| Category | Materials | +|---|---| +| [Metals](metals/README.md) | 19 | +| [Scintillators](scintillators/README.md) | 13 | +| [Plastics](plastics/README.md) | 24 | +| [Ceramics](ceramics/README.md) | 9 | +| [Electronics](electronics/README.md) | 11 | +| [Liquids](liquids/README.md) | 6 | +| [Gases](gases/README.md) | 14 | diff --git a/docs/catalog/ceramics/README.md b/docs/catalog/ceramics/README.md new file mode 100644 index 0000000..1993ddb --- /dev/null +++ b/docs/catalog/ceramics/README.md @@ -0,0 +1,15 @@ +# Ceramics + +9 materials. Click a name for full properties. + +| Material | Preview | Density | E | T_melt | +|---|---|---|---|---| +| [Alumina](alumina.md) | — | 3.95 g/cm³ | 345 GPa | 2072 °C | +| [99.5% Alumina](alumina-al995.md) | — | 3.89 g/cm³ | 345 GPa | 2072 °C | +| [99.9% Alumina](alumina-al999.md) | — | 3.96 g/cm³ | 345 GPa | 2072 °C | +| [Macor](macor.md) | — | 2.52 g/cm³ | 66 GPa | 1000 °C | +| [Zirconia](zirconia.md) | — | 6.0 g/cm³ | 200 GPa | 2715 °C | +| [Glass](glass.md) | — | 2.5 g/cm³ | 70 GPa | 1600 °C | +| [Borosilicate Glass](glass-borosilicate.md) | — | 2.5 g/cm³ | 70 GPa | 1700 °C | +| [Fused Silica](glass-fused_silica.md) | — | 2.2 g/cm³ | 70 GPa | 1600 °C | +| [BK7 Optical Glass](glass-BK7.md) | — | 2.5 g/cm³ | 70 GPa | 1600 °C | diff --git a/docs/catalog/ceramics/alumina-al995.md b/docs/catalog/ceramics/alumina-al995.md new file mode 100644 index 0000000..31bd5c4 --- /dev/null +++ b/docs/catalog/ceramics/alumina-al995.md @@ -0,0 +1,31 @@ +# 99.5% Alumina + +## Identity + +| Field | Value | +|---|---| +| Grade | 995 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 3.89 g/cm³ | +| Young's Modulus | 345 GPa | +| Yield Strength | 240 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 2072 °C | +| Thermal Conductivity | 30 W/(m·K) | +| Specific Heat | 880 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 0.93, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/ceramics/alumina-al999.md b/docs/catalog/ceramics/alumina-al999.md new file mode 100644 index 0000000..465458a --- /dev/null +++ b/docs/catalog/ceramics/alumina-al999.md @@ -0,0 +1,31 @@ +# 99.9% Alumina + +## Identity + +| Field | Value | +|---|---| +| Grade | 999 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 3.96 g/cm³ | +| Young's Modulus | 345 GPa | +| Yield Strength | 240 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 2072 °C | +| Thermal Conductivity | 30 W/(m·K) | +| Specific Heat | 880 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 0.93, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/ceramics/alumina.md b/docs/catalog/ceramics/alumina.md new file mode 100644 index 0000000..97e4b2a --- /dev/null +++ b/docs/catalog/ceramics/alumina.md @@ -0,0 +1,31 @@ +# Alumina + +## Identity + +| Field | Value | +|---|---| +| Formula | `Al2O3` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 3.95 g/cm³ | +| Young's Modulus | 345 GPa | +| Yield Strength | 240 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 2072 °C | +| Thermal Conductivity | 30 W/(m·K) | +| Specific Heat | 880 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 0.93, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/ceramics/glass-BK7.md b/docs/catalog/ceramics/glass-BK7.md new file mode 100644 index 0000000..8baf0f2 --- /dev/null +++ b/docs/catalog/ceramics/glass-BK7.md @@ -0,0 +1,33 @@ +# BK7 Optical Glass + +## Identity + +| Field | Value | +|---|---| +| Grade | BK7 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.5 g/cm³ | +| Young's Modulus | 70 GPa | +| Tensile Strength | 50 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1600 °C | +| Thermal Conductivity | 1.0 W/(m·K) | +| Specific Heat | 800 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.93, 0.95, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.1 | +| IOR | 1.517 | +| Transmission | 0.95 | diff --git a/docs/catalog/ceramics/glass-borosilicate.md b/docs/catalog/ceramics/glass-borosilicate.md new file mode 100644 index 0000000..e1041b2 --- /dev/null +++ b/docs/catalog/ceramics/glass-borosilicate.md @@ -0,0 +1,33 @@ +# Borosilicate Glass + +## Identity + +| Field | Value | +|---|---| +| Grade | borosilicate | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.5 g/cm³ | +| Young's Modulus | 70 GPa | +| Tensile Strength | 50 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1700 °C | +| Thermal Conductivity | 1.0 W/(m·K) | +| Specific Heat | 800 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.93, 0.95, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.1 | +| IOR | 1.52 | +| Transmission | 0.9 | diff --git a/docs/catalog/ceramics/glass-fused_silica.md b/docs/catalog/ceramics/glass-fused_silica.md new file mode 100644 index 0000000..3ce1ef5 --- /dev/null +++ b/docs/catalog/ceramics/glass-fused_silica.md @@ -0,0 +1,34 @@ +# Fused Silica + +## Identity + +| Field | Value | +|---|---| +| Formula | `SiO2` | +| Grade | fused_silica | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.2 g/cm³ | +| Young's Modulus | 70 GPa | +| Tensile Strength | 50 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1600 °C | +| Thermal Conductivity | 1.0 W/(m·K) | +| Specific Heat | 800 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.93, 0.95, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.1 | +| IOR | 1.46 | +| Transmission | 0.99 | diff --git a/docs/catalog/ceramics/glass.md b/docs/catalog/ceramics/glass.md new file mode 100644 index 0000000..72580f4 --- /dev/null +++ b/docs/catalog/ceramics/glass.md @@ -0,0 +1,32 @@ +# Glass + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.5 g/cm³ | +| Young's Modulus | 70 GPa | +| Tensile Strength | 50 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1600 °C | +| Thermal Conductivity | 1.0 W/(m·K) | +| Specific Heat | 800 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.93, 0.95, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.1 | +| IOR | 1.52 | +| Transmission | 0.9 | diff --git a/docs/catalog/ceramics/macor.md b/docs/catalog/ceramics/macor.md new file mode 100644 index 0000000..325a5f6 --- /dev/null +++ b/docs/catalog/ceramics/macor.md @@ -0,0 +1,29 @@ +# Macor + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.52 g/cm³ | +| Young's Modulus | 66 GPa | +| Yield Strength | 60 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1000 °C | +| Thermal Conductivity | 1.46 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.98, 0.98, 0.96, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/ceramics/zirconia.md b/docs/catalog/ceramics/zirconia.md new file mode 100644 index 0000000..2ed2712 --- /dev/null +++ b/docs/catalog/ceramics/zirconia.md @@ -0,0 +1,30 @@ +# Zirconia + +## Identity + +| Field | Value | +|---|---| +| Formula | `ZrO2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 6.0 g/cm³ | +| Young's Modulus | 200 GPa | +| Yield Strength | 1000 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 2715 °C | +| Thermal Conductivity | 2.0 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.88, 0.85, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/electronics/README.md b/docs/catalog/electronics/README.md new file mode 100644 index 0000000..cdcc488 --- /dev/null +++ b/docs/catalog/electronics/README.md @@ -0,0 +1,17 @@ +# Electronics + +11 materials. Click a name for full properties. + +| Material | Preview | Density | +|---|---|---| +| [FR4](fr4.md) | — | 1.86 g/cm³ | +| [Rogers RF Laminate](rogers.md) | — | 1.9 g/cm³ | +| [Rogers 4350B](rogers-r4350b.md) | — | 1.86 g/cm³ | +| [Kapton (Polyimide)](kapton.md) | — | 1.42 g/cm³ | +| [Copper (PCB)](copper_pcb.md) | — | 8.96 g/cm³ | +| [1 oz Copper (35 µm)](copper_pcb-oz1.md) | — | 8.96 g/cm³ | +| [2 oz Copper (70 µm)](copper_pcb-oz2.md) | — | 8.96 g/cm³ | +| [Gold Plated Copper (ENIG)](copper_pcb-gold_plated.md) | — | 8.96 g/cm³ | +| [Solder](solder.md) | — | 8.4 g/cm³ | +| [Sn63Pb37 (60% Tin / 40% Lead)](solder-Sn63Pb37.md) | — | 8.4 g/cm³ | +| [SAC305 (96.5% Tin / 3% Silver / 0.5% Copper)](solder-SAC305.md) | — | 7.3 g/cm³ | diff --git a/docs/catalog/electronics/copper_pcb-gold_plated.md b/docs/catalog/electronics/copper_pcb-gold_plated.md new file mode 100644 index 0000000..b446cd2 --- /dev/null +++ b/docs/catalog/electronics/copper_pcb-gold_plated.md @@ -0,0 +1,21 @@ +# Gold Plated Copper (ENIG) + +## Identity + +| Field | Value | +|---|---| +| Treatment | gold_plated | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.96 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(1.0, 0.84, 0.0, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.2 | diff --git a/docs/catalog/electronics/copper_pcb-oz1.md b/docs/catalog/electronics/copper_pcb-oz1.md new file mode 100644 index 0000000..9553834 --- /dev/null +++ b/docs/catalog/electronics/copper_pcb-oz1.md @@ -0,0 +1,21 @@ +# 1 oz Copper (35 µm) + +## Identity + +| Field | Value | +|---|---| +| Grade | 1oz | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.96 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.72, 0.45, 0.2, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.35 | diff --git a/docs/catalog/electronics/copper_pcb-oz2.md b/docs/catalog/electronics/copper_pcb-oz2.md new file mode 100644 index 0000000..eb09f35 --- /dev/null +++ b/docs/catalog/electronics/copper_pcb-oz2.md @@ -0,0 +1,21 @@ +# 2 oz Copper (70 µm) + +## Identity + +| Field | Value | +|---|---| +| Grade | 2oz | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.96 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.72, 0.45, 0.2, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.35 | diff --git a/docs/catalog/electronics/copper_pcb.md b/docs/catalog/electronics/copper_pcb.md new file mode 100644 index 0000000..bf8b36b --- /dev/null +++ b/docs/catalog/electronics/copper_pcb.md @@ -0,0 +1,20 @@ +# Copper (PCB) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.96 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.72, 0.45, 0.2, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.35 | diff --git a/docs/catalog/electronics/fr4.md b/docs/catalog/electronics/fr4.md new file mode 100644 index 0000000..66ca99c --- /dev/null +++ b/docs/catalog/electronics/fr4.md @@ -0,0 +1,22 @@ +# FR4 + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.86 g/cm³ | +| Young's Modulus | 19 GPa | +| Tensile Strength | 60 MPa | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.15, 0.15, 0.15, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.7 | diff --git a/docs/catalog/electronics/kapton.md b/docs/catalog/electronics/kapton.md new file mode 100644 index 0000000..502a4da --- /dev/null +++ b/docs/catalog/electronics/kapton.md @@ -0,0 +1,28 @@ +# Kapton (Polyimide) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.42 g/cm³ | +| Tensile Strength | 230 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 400 °C | +| Thermal Conductivity | 0.12 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.85, 0.4, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/electronics/rogers-r4350b.md b/docs/catalog/electronics/rogers-r4350b.md new file mode 100644 index 0000000..63bb0dd --- /dev/null +++ b/docs/catalog/electronics/rogers-r4350b.md @@ -0,0 +1,21 @@ +# Rogers 4350B + +## Identity + +| Field | Value | +|---|---| +| Grade | 4350B | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.86 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.2, 0.2, 0.2, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/electronics/rogers.md b/docs/catalog/electronics/rogers.md new file mode 100644 index 0000000..209f8e9 --- /dev/null +++ b/docs/catalog/electronics/rogers.md @@ -0,0 +1,20 @@ +# Rogers RF Laminate + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.9 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.2, 0.2, 0.2, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/electronics/solder-SAC305.md b/docs/catalog/electronics/solder-SAC305.md new file mode 100644 index 0000000..31db408 --- /dev/null +++ b/docs/catalog/electronics/solder-SAC305.md @@ -0,0 +1,27 @@ +# SAC305 (96.5% Tin / 3% Silver / 0.5% Copper) + +## Identity + +| Field | Value | +|---|---| +| Grade | SAC305 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 7.3 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 217 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.7, 0.7, 0.7, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/electronics/solder-Sn63Pb37.md b/docs/catalog/electronics/solder-Sn63Pb37.md new file mode 100644 index 0000000..f2b8561 --- /dev/null +++ b/docs/catalog/electronics/solder-Sn63Pb37.md @@ -0,0 +1,27 @@ +# Sn63Pb37 (60% Tin / 40% Lead) + +## Identity + +| Field | Value | +|---|---| +| Grade | Sn63Pb37 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.4 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 183 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.7, 0.7, 0.7, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/electronics/solder.md b/docs/catalog/electronics/solder.md new file mode 100644 index 0000000..29957e0 --- /dev/null +++ b/docs/catalog/electronics/solder.md @@ -0,0 +1,20 @@ +# Solder + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.4 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.7, 0.7, 0.7, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/gases/README.md b/docs/catalog/gases/README.md new file mode 100644 index 0000000..6f66234 --- /dev/null +++ b/docs/catalog/gases/README.md @@ -0,0 +1,20 @@ +# Gases + +14 materials. Click a name for full properties. + +| Material | Preview | Density | +|---|---|---| +| [Air](air.md) | — | 0.001204 g/cm³ | +| [Nitrogen](nitrogen.md) | — | 0.001165 g/cm³ | +| [Liquid Nitrogen](nitrogen-liquid.md) | — | 0.808 g/cm³ | +| [Oxygen](oxygen.md) | — | 0.001331 g/cm³ | +| [Argon](argon.md) | — | 0.001662 g/cm³ | +| [Carbon Dioxide](co2.md) | — | 0.001839 g/cm³ | +| [Dry Ice](co2-dry_ice.md) | — | 1.56 g/cm³ | +| [Helium](helium.md) | — | 0.000166 g/cm³ | +| [Liquid Helium](helium-liquid.md) | — | 0.125 g/cm³ | +| [Hydrogen](hydrogen.md) | — | 8.38e-05 g/cm³ | +| [Neon](neon.md) | — | 0.000839 g/cm³ | +| [Xenon](xenon.md) | — | 0.005458 g/cm³ | +| [Methane](methane.md) | — | 0.000668 g/cm³ | +| [Vacuum](vacuum.md) | — | — | diff --git a/docs/catalog/gases/air.md b/docs/catalog/gases/air.md new file mode 100644 index 0000000..0a09ed3 --- /dev/null +++ b/docs/catalog/gases/air.md @@ -0,0 +1,23 @@ +# Air + +## Identity + +| Field | Value | +|---|---| +| Formula | `N2O2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.001204 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.01)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000293 | +| Transmission | 0.99 | diff --git a/docs/catalog/gases/argon.md b/docs/catalog/gases/argon.md new file mode 100644 index 0000000..d79f564 --- /dev/null +++ b/docs/catalog/gases/argon.md @@ -0,0 +1,23 @@ +# Argon + +## Identity + +| Field | Value | +|---|---| +| Formula | `Ar` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.001662 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.01)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000281 | +| Transmission | 0.99 | diff --git a/docs/catalog/gases/co2-dry_ice.md b/docs/catalog/gases/co2-dry_ice.md new file mode 100644 index 0000000..24841fd --- /dev/null +++ b/docs/catalog/gases/co2-dry_ice.md @@ -0,0 +1,23 @@ +# Dry Ice + +## Identity + +| Field | Value | +|---|---| +| Treatment | solid | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.56 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 1.0, 0.7)` | +| Metallic | 0.0 | +| Roughness | 0.3 | +| IOR | 1.000449 | +| Transmission | 0.7 | diff --git a/docs/catalog/gases/co2.md b/docs/catalog/gases/co2.md new file mode 100644 index 0000000..c36e69f --- /dev/null +++ b/docs/catalog/gases/co2.md @@ -0,0 +1,23 @@ +# Carbon Dioxide + +## Identity + +| Field | Value | +|---|---| +| Formula | `CO2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.001839 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.01)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000449 | +| Transmission | 0.99 | diff --git a/docs/catalog/gases/helium-liquid.md b/docs/catalog/gases/helium-liquid.md new file mode 100644 index 0000000..2e51e00 --- /dev/null +++ b/docs/catalog/gases/helium-liquid.md @@ -0,0 +1,23 @@ +# Liquid Helium + +## Identity + +| Field | Value | +|---|---| +| Treatment | cryogenic | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.125 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.85, 0.9, 0.95, 0.3)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000035 | +| Transmission | 0.95 | diff --git a/docs/catalog/gases/helium.md b/docs/catalog/gases/helium.md new file mode 100644 index 0000000..33f24e5 --- /dev/null +++ b/docs/catalog/gases/helium.md @@ -0,0 +1,23 @@ +# Helium + +## Identity + +| Field | Value | +|---|---| +| Formula | `He` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.000166 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.005)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000035 | +| Transmission | 0.995 | diff --git a/docs/catalog/gases/hydrogen.md b/docs/catalog/gases/hydrogen.md new file mode 100644 index 0000000..b82fef7 --- /dev/null +++ b/docs/catalog/gases/hydrogen.md @@ -0,0 +1,23 @@ +# Hydrogen + +## Identity + +| Field | Value | +|---|---| +| Formula | `H2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.38e-05 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.005)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000132 | +| Transmission | 0.995 | diff --git a/docs/catalog/gases/methane.md b/docs/catalog/gases/methane.md new file mode 100644 index 0000000..78c1a90 --- /dev/null +++ b/docs/catalog/gases/methane.md @@ -0,0 +1,23 @@ +# Methane + +## Identity + +| Field | Value | +|---|---| +| Formula | `CH4` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.000668 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.01)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000444 | +| Transmission | 0.99 | diff --git a/docs/catalog/gases/neon.md b/docs/catalog/gases/neon.md new file mode 100644 index 0000000..33bfecb --- /dev/null +++ b/docs/catalog/gases/neon.md @@ -0,0 +1,23 @@ +# Neon + +## Identity + +| Field | Value | +|---|---| +| Formula | `Ne` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.000839 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(1.0, 0.7, 0.3, 0.02)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000067 | +| Transmission | 0.98 | diff --git a/docs/catalog/gases/nitrogen-liquid.md b/docs/catalog/gases/nitrogen-liquid.md new file mode 100644 index 0000000..95ff529 --- /dev/null +++ b/docs/catalog/gases/nitrogen-liquid.md @@ -0,0 +1,23 @@ +# Liquid Nitrogen + +## Identity + +| Field | Value | +|---|---| +| Treatment | cryogenic | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.808 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.85, 0.9, 0.95, 0.4)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000298 | +| Transmission | 0.9 | diff --git a/docs/catalog/gases/nitrogen.md b/docs/catalog/gases/nitrogen.md new file mode 100644 index 0000000..7461a26 --- /dev/null +++ b/docs/catalog/gases/nitrogen.md @@ -0,0 +1,23 @@ +# Nitrogen + +## Identity + +| Field | Value | +|---|---| +| Formula | `N2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.001165 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.01)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000298 | +| Transmission | 0.99 | diff --git a/docs/catalog/gases/oxygen.md b/docs/catalog/gases/oxygen.md new file mode 100644 index 0000000..83fdc0a --- /dev/null +++ b/docs/catalog/gases/oxygen.md @@ -0,0 +1,23 @@ +# Oxygen + +## Identity + +| Field | Value | +|---|---| +| Formula | `O2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.001331 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.01)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000271 | +| Transmission | 0.99 | diff --git a/docs/catalog/gases/vacuum.md b/docs/catalog/gases/vacuum.md new file mode 100644 index 0000000..463b502 --- /dev/null +++ b/docs/catalog/gases/vacuum.md @@ -0,0 +1,22 @@ +# Vacuum + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.0 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.0, 0.0, 0.0, 0.0)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.0 | +| Transmission | 1.0 | diff --git a/docs/catalog/gases/xenon.md b/docs/catalog/gases/xenon.md new file mode 100644 index 0000000..f716ba1 --- /dev/null +++ b/docs/catalog/gases/xenon.md @@ -0,0 +1,23 @@ +# Xenon + +## Identity + +| Field | Value | +|---|---| +| Formula | `Xe` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.005458 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.8, 0.85, 1.0, 0.02)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.000702 | +| Transmission | 0.98 | diff --git a/docs/catalog/liquids/README.md b/docs/catalog/liquids/README.md new file mode 100644 index 0000000..78ae3de --- /dev/null +++ b/docs/catalog/liquids/README.md @@ -0,0 +1,12 @@ +# Liquids + +6 materials. Click a name for full properties. + +| Material | Preview | Density | n (IOR) | +|---|---|---|---| +| [Water](water.md) | — | 0.998 g/cm³ | 1.333 | +| [Ice](water-ice.md) | — | 0.917 g/cm³ | 1.333 | +| [Heavy Water (D2O)](heavy_water.md) | — | 1.107 g/cm³ | 1.328 | +| [Mineral Oil](mineral_oil.md) | — | 0.85 g/cm³ | 1.47 | +| [Glycerol](glycerol.md) | — | 1.261 g/cm³ | 1.473 | +| [Silicone Oil](silicone_oil.md) | — | 0.97 g/cm³ | 1.4 | diff --git a/docs/catalog/liquids/glycerol.md b/docs/catalog/liquids/glycerol.md new file mode 100644 index 0000000..0332039 --- /dev/null +++ b/docs/catalog/liquids/glycerol.md @@ -0,0 +1,31 @@ +# Glycerol + +## Identity + +| Field | Value | +|---|---| +| Formula | `C3H8O3` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.261 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 18 °C | +| Thermal Conductivity | 0.29 W/(m·K) | +| Specific Heat | 2430 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 0.9, 0.5)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.473 | +| Transmission | 0.9 | diff --git a/docs/catalog/liquids/heavy_water.md b/docs/catalog/liquids/heavy_water.md new file mode 100644 index 0000000..fe8c5c9 --- /dev/null +++ b/docs/catalog/liquids/heavy_water.md @@ -0,0 +1,31 @@ +# Heavy Water (D2O) + +## Identity + +| Field | Value | +|---|---| +| Formula | `D2O` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.107 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 3.82 °C | +| Thermal Conductivity | 0.595 W/(m·K) | +| Specific Heat | 4210 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.7, 0.85, 0.95, 0.3)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.328 | +| Transmission | 0.95 | diff --git a/docs/catalog/liquids/mineral_oil.md b/docs/catalog/liquids/mineral_oil.md new file mode 100644 index 0000000..cf57d2d --- /dev/null +++ b/docs/catalog/liquids/mineral_oil.md @@ -0,0 +1,22 @@ +# Mineral Oil + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.85 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.9, 0.7, 0.4)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.47 | +| Transmission | 0.9 | diff --git a/docs/catalog/liquids/silicone_oil.md b/docs/catalog/liquids/silicone_oil.md new file mode 100644 index 0000000..26bbb7a --- /dev/null +++ b/docs/catalog/liquids/silicone_oil.md @@ -0,0 +1,22 @@ +# Silicone Oil + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.97 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.9, 0.4)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.4 | +| Transmission | 0.9 | diff --git a/docs/catalog/liquids/water-ice.md b/docs/catalog/liquids/water-ice.md new file mode 100644 index 0000000..ccf2b07 --- /dev/null +++ b/docs/catalog/liquids/water-ice.md @@ -0,0 +1,31 @@ +# Ice + +## Identity + +| Field | Value | +|---|---| +| Treatment | frozen | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.917 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 0 °C | +| Thermal Conductivity | 2.22 W/(m·K) | +| Specific Heat | 2090 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.95, 1.0, 0.7)` | +| Metallic | 0.0 | +| Roughness | 0.1 | +| IOR | 1.333 | +| Transmission | 0.8 | diff --git a/docs/catalog/liquids/water.md b/docs/catalog/liquids/water.md new file mode 100644 index 0000000..a1c1f8d --- /dev/null +++ b/docs/catalog/liquids/water.md @@ -0,0 +1,31 @@ +# Water + +## Identity + +| Field | Value | +|---|---| +| Formula | `H2O` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.998 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 0 °C | +| Thermal Conductivity | 0.598 W/(m·K) | +| Specific Heat | 4182 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.7, 0.85, 0.95, 0.3)` | +| Metallic | 0.0 | +| Roughness | 0.0 | +| IOR | 1.333 | +| Transmission | 0.95 | diff --git a/docs/catalog/metals/README.md b/docs/catalog/metals/README.md new file mode 100644 index 0000000..ef2ed1d --- /dev/null +++ b/docs/catalog/metals/README.md @@ -0,0 +1,25 @@ +# Metals + +19 materials. Click a name for full properties. + +| Material | Preview | Density | Yield | Tensile | E | T_melt | +|---|---|---|---|---|---|---| +| [Stainless Steel](stainless.md) | ![](thumbs/stainless.png) | 8.0 g/cm³ | — | — | 193 GPa | 1450 °C | +| [Stainless Steel 304](stainless-s304.md) | — | 8.0 g/cm³ | 170 MPa | 515 MPa | 193 GPa | 1450 °C | +| [Stainless Steel 316L](stainless-s316L.md) | — | 8.0 g/cm³ | 170 MPa | 485 MPa | 193 GPa | 1450 °C | +| [Stainless Steel 303](stainless-s303.md) | — | 8.0 g/cm³ | 205 MPa | 515 MPa | 193 GPa | 1450 °C | +| [Stainless Steel 17-4 PH](stainless-s17_4PH.md) | — | 7.8 g/cm³ | 1170 MPa | 1310 MPa | 193 GPa | 1450 °C | +| [Aluminum](aluminum.md) | ![](thumbs/aluminum.png) | 2.7 g/cm³ | — | — | 69 GPa | 660 °C | +| [Aluminum 6061-T6](aluminum-a6061.md) | — | 2.7 g/cm³ | 276 MPa | 310 MPa | 69 GPa | 660 °C | +| [Aluminum 6063](aluminum-a6063.md) | — | 2.69 g/cm³ | 48 MPa | 130 MPa | 69 GPa | 660 °C | +| [Aluminum 7075](aluminum-a7075.md) | — | 2.81 g/cm³ | — | — | 72 GPa | 660 °C | +| [Aluminum 2024](aluminum-a2024.md) | — | 2.78 g/cm³ | — | — | 69 GPa | 660 °C | +| [Copper](copper.md) | ![](thumbs/copper.png) | 8.96 g/cm³ | 40 MPa | 200 MPa | 110 GPa | 1085 °C | +| [OFHC Copper (Oxygen-Free High Conductivity)](copper-OFHC.md) | — | 8.94 g/cm³ | 40 MPa | 200 MPa | 110 GPa | 1085 °C | +| [Tungsten](tungsten.md) | — | 19.3 g/cm³ | — | — | 411 GPa | 3422 °C | +| [Tungsten 99.95%](tungsten-pure.md) | — | 19.3 g/cm³ | — | — | 411 GPa | 3422 °C | +| [Tungsten Heavy Alloy (90% W)](tungsten-W90.md) | — | 17.0 g/cm³ | — | — | 411 GPa | 3422 °C | +| [Lead](lead.md) | — | 11.34 g/cm³ | 12 MPa | — | 16 GPa | 327 °C | +| [Titanium](titanium.md) | ![](thumbs/titanium.png) | 4.51 g/cm³ | 880 MPa | 950 MPa | 103 GPa | 1668 °C | +| [Ti-6Al-4V (Grade 5)](titanium-grade5.md) | — | 4.43 g/cm³ | 880 MPa | 950 MPa | 103 GPa | 1668 °C | +| [Brass (Cu-Zn)](brass.md) | ![](thumbs/brass.png) | 8.5 g/cm³ | 200 MPa | — | 97 GPa | 900 °C | diff --git a/docs/catalog/metals/aluminum-a2024.md b/docs/catalog/metals/aluminum-a2024.md new file mode 100644 index 0000000..7591ec3 --- /dev/null +++ b/docs/catalog/metals/aluminum-a2024.md @@ -0,0 +1,31 @@ +# Aluminum 2024 + +## Identity + +| Field | Value | +|---|---| +| Grade | 2024 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.78 g/cm³ | +| Young's Modulus | 69 GPa | +| Poisson's Ratio | 0.33 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 660 °C | +| Thermal Conductivity | 235 W/(m·K) | +| Specific Heat | 900 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.88, 0.88, 0.88, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/metals/aluminum-a6061.md b/docs/catalog/metals/aluminum-a6061.md new file mode 100644 index 0000000..fa36878 --- /dev/null +++ b/docs/catalog/metals/aluminum-a6061.md @@ -0,0 +1,45 @@ +# Aluminum 6061-T6 + +## Identity + +| Field | Value | +|---|---| +| Grade | 6061 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.7 g/cm³ | +| Young's Modulus | 69 GPa | +| Yield Strength | 276 MPa | +| Tensile Strength | 310 MPa | +| Poisson's Ratio | 0.33 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 660 °C | +| Thermal Conductivity | 235 W/(m·K) | +| Specific Heat | 900 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.88, 0.88, 0.88, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | + +## Composition + +| Element | Fraction | +|---|---| +| Al | 0.972 | +| Mg | 0.01 | +| Si | 0.006 | +| Cu | 0.004 | +| Fe | 0.003 | +| Zn | 0.003 | +| Cr | 0.002 | diff --git a/docs/catalog/metals/aluminum-a6063.md b/docs/catalog/metals/aluminum-a6063.md new file mode 100644 index 0000000..7cf7f1d --- /dev/null +++ b/docs/catalog/metals/aluminum-a6063.md @@ -0,0 +1,47 @@ +# Aluminum 6063 + +## Identity + +| Field | Value | +|---|---| +| Grade | 6063 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.69 g/cm³ | +| Young's Modulus | 69 GPa | +| Yield Strength | 48 MPa | +| Tensile Strength | 130 MPa | +| Poisson's Ratio | 0.33 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 660 °C | +| Thermal Conductivity | 235 W/(m·K) | +| Specific Heat | 900 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.88, 0.88, 0.88, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | + +## Composition + +| Element | Fraction | +|---|---| +| Al | 0.982 | +| Mg | 0.007 ± 0.00225 | +| Si | 0.004 ± 0.002 | +| Fe | 0.002 | +| Cu | 0.001 | +| Mn | 0.001 | +| Zn | 0.001 | +| Ti | 0.001 | +| Cr | 0.001 | diff --git a/docs/catalog/metals/aluminum-a7075.md b/docs/catalog/metals/aluminum-a7075.md new file mode 100644 index 0000000..77eef73 --- /dev/null +++ b/docs/catalog/metals/aluminum-a7075.md @@ -0,0 +1,42 @@ +# Aluminum 7075 + +## Identity + +| Field | Value | +|---|---| +| Grade | 7075 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.81 g/cm³ | +| Young's Modulus | 72 GPa | +| Poisson's Ratio | 0.33 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 660 °C | +| Thermal Conductivity | 235 W/(m·K) | +| Specific Heat | 900 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.88, 0.88, 0.88, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | + +## Composition + +| Element | Fraction | +|---|---| +| Al | 0.9 | +| Zn | 0.056 | +| Mg | 0.025 | +| Cu | 0.016 | +| Cr | 0.002 | +| Fe | 0.001 | diff --git a/docs/catalog/metals/aluminum.md b/docs/catalog/metals/aluminum.md new file mode 100644 index 0000000..ca9ebd0 --- /dev/null +++ b/docs/catalog/metals/aluminum.md @@ -0,0 +1,41 @@ +# Aluminum + +![Aluminum](thumbs/aluminum.png) + +## Identity + +| Field | Value | +|---|---| +| Formula | `Al` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.7 g/cm³ | +| Young's Modulus | 69 GPa | +| Poisson's Ratio | 0.33 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 660 °C | +| Thermal Conductivity | 235 W/(m·K) | +| Specific Heat | 900 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.88, 0.88, 0.88, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | + +## Visual (mat-vis) + +| Field | Value | +|---|---| +| Source ID | `ambientcg/Metal049A` | +| Finish | smooth | +| Available Finishes | smooth, machined | diff --git a/docs/catalog/metals/brass.md b/docs/catalog/metals/brass.md new file mode 100644 index 0000000..5aeb23c --- /dev/null +++ b/docs/catalog/metals/brass.md @@ -0,0 +1,45 @@ +# Brass (Cu-Zn) + +![Brass (Cu-Zn)](thumbs/brass.png) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.5 g/cm³ | +| Young's Modulus | 97 GPa | +| Yield Strength | 200 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 900 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.88, 0.78, 0.5, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.25 | + +## Visual (mat-vis) + +| Field | Value | +|---|---| +| Source ID | `ambientcg/Metal008` | +| Finish | polished | +| Available Finishes | polished, oxidized | + +## Composition + +| Element | Fraction | +|---|---| +| Cu | 0.65 | +| Zn | 0.35 | diff --git a/docs/catalog/metals/copper-OFHC.md b/docs/catalog/metals/copper-OFHC.md new file mode 100644 index 0000000..93776b2 --- /dev/null +++ b/docs/catalog/metals/copper-OFHC.md @@ -0,0 +1,32 @@ +# OFHC Copper (Oxygen-Free High Conductivity) + +## Identity + +| Field | Value | +|---|---| +| Grade | OFHC | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.94 g/cm³ | +| Young's Modulus | 110 GPa | +| Yield Strength | 40 MPa | +| Tensile Strength | 200 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1085 °C | +| Thermal Conductivity | 398 W/(m·K) | +| Specific Heat | 385 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.72, 0.45, 0.2, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | diff --git a/docs/catalog/metals/copper.md b/docs/catalog/metals/copper.md new file mode 100644 index 0000000..d46d543 --- /dev/null +++ b/docs/catalog/metals/copper.md @@ -0,0 +1,42 @@ +# Copper + +![Copper](thumbs/copper.png) + +## Identity + +| Field | Value | +|---|---| +| Formula | `Cu` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.96 g/cm³ | +| Young's Modulus | 110 GPa | +| Yield Strength | 40 MPa | +| Tensile Strength | 200 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1085 °C | +| Thermal Conductivity | 398 W/(m·K) | +| Specific Heat | 385 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.72, 0.45, 0.2, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Visual (mat-vis) + +| Field | Value | +|---|---| +| Source ID | `ambientcg/Metal043A` | +| Finish | polished | +| Available Finishes | polished, oxidized, aged | diff --git a/docs/catalog/metals/lead.md b/docs/catalog/metals/lead.md new file mode 100644 index 0000000..e223788 --- /dev/null +++ b/docs/catalog/metals/lead.md @@ -0,0 +1,30 @@ +# Lead + +## Identity + +| Field | Value | +|---|---| +| Formula | `Pb` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 11.34 g/cm³ | +| Young's Modulus | 16 GPa | +| Yield Strength | 12 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 327 °C | +| Thermal Conductivity | 35.3 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.3, 0.3, 0.33, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/metals/stainless-s17_4PH.md b/docs/catalog/metals/stainless-s17_4PH.md new file mode 100644 index 0000000..76b72b4 --- /dev/null +++ b/docs/catalog/metals/stainless-s17_4PH.md @@ -0,0 +1,46 @@ +# Stainless Steel 17-4 PH + +## Identity + +| Field | Value | +|---|---| +| Grade | 17-4 PH | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 7.8 g/cm³ | +| Young's Modulus | 193 GPa | +| Yield Strength | 1170 MPa | +| Tensile Strength | 1310 MPa | +| Poisson's Ratio | 0.27 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1450 °C | +| Thermal Conductivity | 15.1 W/(m·K) | +| Specific Heat | 500 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.75, 0.75, 0.77, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Composition + +| Element | Fraction | +|---|---| +| Fe | 0.738 | +| Cr | 0.16 | +| Ni | 0.04 | +| Cu | 0.035 | +| Mn | 0.01 | +| Si | 0.01 | +| C | 0.004 | +| Nb | 0.003 | diff --git a/docs/catalog/metals/stainless-s303.md b/docs/catalog/metals/stainless-s303.md new file mode 100644 index 0000000..d70fc4b --- /dev/null +++ b/docs/catalog/metals/stainless-s303.md @@ -0,0 +1,33 @@ +# Stainless Steel 303 + +## Identity + +| Field | Value | +|---|---| +| Grade | 303 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.0 g/cm³ | +| Young's Modulus | 193 GPa | +| Yield Strength | 205 MPa | +| Tensile Strength | 515 MPa | +| Poisson's Ratio | 0.27 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1450 °C | +| Thermal Conductivity | 15.1 W/(m·K) | +| Specific Heat | 500 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.75, 0.75, 0.77, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | diff --git a/docs/catalog/metals/stainless-s304.md b/docs/catalog/metals/stainless-s304.md new file mode 100644 index 0000000..b1b399f --- /dev/null +++ b/docs/catalog/metals/stainless-s304.md @@ -0,0 +1,44 @@ +# Stainless Steel 304 + +## Identity + +| Field | Value | +|---|---| +| Grade | 304 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.0 g/cm³ | +| Young's Modulus | 193 GPa | +| Yield Strength | 170 MPa | +| Tensile Strength | 515 MPa | +| Poisson's Ratio | 0.27 | +| Hardness (Vickers) | 217 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1450 °C | +| Thermal Conductivity | 15.1 W/(m·K) | +| Specific Heat | 500 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.75, 0.75, 0.77, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Composition + +| Element | Fraction | +|---|---| +| Fe | 0.69 | +| Cr | 0.19 | +| Ni | 0.095 | +| Mn | 0.02 | +| Si | 0.005 | diff --git a/docs/catalog/metals/stainless-s316L.md b/docs/catalog/metals/stainless-s316L.md new file mode 100644 index 0000000..d95afd5 --- /dev/null +++ b/docs/catalog/metals/stainless-s316L.md @@ -0,0 +1,46 @@ +# Stainless Steel 316L + +## Identity + +| Field | Value | +|---|---| +| Grade | 316L | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.0 g/cm³ | +| Young's Modulus | 193 GPa | +| Yield Strength | 170 MPa | +| Tensile Strength | 485 MPa | +| Poisson's Ratio | 0.27 | +| Hardness (Vickers) | 217 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1450 °C | +| Thermal Conductivity | 15.1 W/(m·K) | +| Specific Heat | 500 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.75, 0.75, 0.77, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Composition + +| Element | Fraction | +|---|---| +| Fe | 0.65 | +| Cr | 0.17 | +| Ni | 0.12 | +| Mo | 0.025 | +| Mn | 0.02 | +| Si | 0.01 | +| C | 0.005 | diff --git a/docs/catalog/metals/stainless.md b/docs/catalog/metals/stainless.md new file mode 100644 index 0000000..ac78ab8 --- /dev/null +++ b/docs/catalog/metals/stainless.md @@ -0,0 +1,49 @@ +# Stainless Steel + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.0 g/cm³ | +| Young's Modulus | 193 GPa | +| Poisson's Ratio | 0.27 | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1450 °C | +| Thermal Conductivity | 15.1 W/(m·K) | +| Specific Heat | 500 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.75, 0.75, 0.77, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Visual (mat-vis) + +| Field | Value | +|---|---| +| Source ID | `ambientcg/Metal012` | +| Finish | brushed | +| Available Finishes | brushed, polished, dirty | + +## Composition + +| Element | Fraction | +|---|---| +| Fe | 0.68 | +| Cr | 0.18 | +| Ni | 0.1 | +| Mn | 0.02 | +| Si | 0.01 | +| C | 0.005 | diff --git a/docs/catalog/metals/thumbs/aluminum.png b/docs/catalog/metals/thumbs/aluminum.png new file mode 100644 index 0000000..e0b7e39 Binary files /dev/null and b/docs/catalog/metals/thumbs/aluminum.png differ diff --git a/docs/catalog/metals/thumbs/brass.png b/docs/catalog/metals/thumbs/brass.png new file mode 100644 index 0000000..2824391 Binary files /dev/null and b/docs/catalog/metals/thumbs/brass.png differ diff --git a/docs/catalog/metals/thumbs/copper.png b/docs/catalog/metals/thumbs/copper.png new file mode 100644 index 0000000..09ff8d3 Binary files /dev/null and b/docs/catalog/metals/thumbs/copper.png differ diff --git a/docs/catalog/metals/thumbs/titanium.png b/docs/catalog/metals/thumbs/titanium.png new file mode 100644 index 0000000..e0b7e39 Binary files /dev/null and b/docs/catalog/metals/thumbs/titanium.png differ diff --git a/docs/catalog/metals/titanium-grade5.md b/docs/catalog/metals/titanium-grade5.md new file mode 100644 index 0000000..cfb4b55 --- /dev/null +++ b/docs/catalog/metals/titanium-grade5.md @@ -0,0 +1,39 @@ +# Ti-6Al-4V (Grade 5) + +## Identity + +| Field | Value | +|---|---| +| Grade | grade5 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 4.43 g/cm³ | +| Young's Modulus | 103 GPa | +| Yield Strength | 880 MPa | +| Tensile Strength | 950 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1668 °C | +| Thermal Conductivity | 21.9 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.6, 0.6, 0.6, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Composition + +| Element | Fraction | +|---|---| +| Ti | 0.9 | +| Al | 0.06 | +| V | 0.04 | diff --git a/docs/catalog/metals/titanium.md b/docs/catalog/metals/titanium.md new file mode 100644 index 0000000..eb5ec53 --- /dev/null +++ b/docs/catalog/metals/titanium.md @@ -0,0 +1,41 @@ +# Titanium + +![Titanium](thumbs/titanium.png) + +## Identity + +| Field | Value | +|---|---| +| Formula | `Ti` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 4.51 g/cm³ | +| Young's Modulus | 103 GPa | +| Yield Strength | 880 MPa | +| Tensile Strength | 950 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1668 °C | +| Thermal Conductivity | 21.9 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.6, 0.6, 0.6, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.3 | + +## Visual (mat-vis) + +| Field | Value | +|---|---| +| Source ID | `ambientcg/Metal049A` | +| Finish | smooth | +| Available Finishes | smooth | diff --git a/docs/catalog/metals/tungsten-W90.md b/docs/catalog/metals/tungsten-W90.md new file mode 100644 index 0000000..5e95d14 --- /dev/null +++ b/docs/catalog/metals/tungsten-W90.md @@ -0,0 +1,38 @@ +# Tungsten Heavy Alloy (90% W) + +## Identity + +| Field | Value | +|---|---| +| Grade | W90 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 17.0 g/cm³ | +| Young's Modulus | 411 GPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 3422 °C | +| Thermal Conductivity | 173 W/(m·K) | +| Specific Heat | 133 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.2, 0.2, 0.22, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | + +## Composition + +| Element | Fraction | +|---|---| +| W | 0.9 | +| Ni | 0.07 | +| Fe | 0.03 | diff --git a/docs/catalog/metals/tungsten-pure.md b/docs/catalog/metals/tungsten-pure.md new file mode 100644 index 0000000..e9a2f94 --- /dev/null +++ b/docs/catalog/metals/tungsten-pure.md @@ -0,0 +1,30 @@ +# Tungsten 99.95% + +## Identity + +| Field | Value | +|---|---| +| Grade | pure | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 19.3 g/cm³ | +| Young's Modulus | 411 GPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 3422 °C | +| Thermal Conductivity | 173 W/(m·K) | +| Specific Heat | 133 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.2, 0.2, 0.22, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/metals/tungsten.md b/docs/catalog/metals/tungsten.md new file mode 100644 index 0000000..0ace466 --- /dev/null +++ b/docs/catalog/metals/tungsten.md @@ -0,0 +1,30 @@ +# Tungsten + +## Identity + +| Field | Value | +|---|---| +| Formula | `W` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 19.3 g/cm³ | +| Young's Modulus | 411 GPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 3422 °C | +| Thermal Conductivity | 173 W/(m·K) | +| Specific Heat | 133 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.2, 0.2, 0.22, 1.0)` | +| Metallic | 1.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/plastics/README.md b/docs/catalog/plastics/README.md new file mode 100644 index 0000000..e050265 --- /dev/null +++ b/docs/catalog/plastics/README.md @@ -0,0 +1,30 @@ +# Plastics + +24 materials. Click a name for full properties. + +| Material | Preview | Density | Yield | T_melt | k | +|---|---|---|---|---|---| +| [PEEK](peek.md) | — | 1.32 g/cm³ | 100 MPa | 334 °C | 0.25 W/m·K | +| [PEEK Unfilled](peek-unfilled.md) | — | 1.32 g/cm³ | 100 MPa | 334 °C | 0.25 W/m·K | +| [PEEK 30% Glass Filled](peek-GF30.md) | — | 1.53 g/cm³ | 170 MPa | 334 °C | 0.25 W/m·K | +| [PEEK 30% Carbon Filled](peek-CF30.md) | — | 1.41 g/cm³ | 100 MPa | 334 °C | 0.25 W/m·K | +| [Victrex PEEK](peek-victrex.md) | — | 1.32 g/cm³ | 100 MPa | 334 °C | 0.25 W/m·K | +| [Delrin (POM)](delrin.md) | — | 1.41 g/cm³ | 70 MPa | 165 °C | 0.25 W/m·K | +| [Ultem (PEI)](ultem.md) | — | 1.27 g/cm³ | 90 MPa | 340 °C | 0.22 W/m·K | +| [PTFE (Teflon)](ptfe.md) | — | 2.15 g/cm³ | 20 MPa | 327 °C | 0.24 W/m·K | +| [PTFE Reflector Tape](ptfe-reflector.md) | — | 2.15 g/cm³ | 20 MPa | 327 °C | 0.24 W/m·K | +| [ESR (3M Vikuiti)](esr.md) | — | 1.0 g/cm³ | — | — | — | +| [Nylon 6](nylon.md) | — | 1.13 g/cm³ | 45 MPa | 215 °C | — | +| [PLA (Polylactic Acid)](pla.md) | — | 1.25 g/cm³ | 50 MPa | 160 °C | — | +| [ABS (Acrylonitrile Butadiene Styrene)](abs.md) | — | 1.05 g/cm³ | 40 MPa | 225 °C | — | +| [PETG](petg.md) | — | 1.27 g/cm³ | — | 225 °C | — | +| [TPU (Thermoplastic Polyurethane)](tpu.md) | — | 1.21 g/cm³ | — | — | — | +| [Vespel (Polyimide)](vespel.md) | — | 1.42 g/cm³ | 70 MPa | 400 °C | — | +| [Torlon (PAI)](torlon.md) | — | 1.45 g/cm³ | 110 MPa | 330 °C | — | +| [PCTFE (Polychlorotrifluoroethylene)](pctfe.md) | — | 2.13 g/cm³ | 50 MPa | 217 °C | — | +| [PMMA (Acrylic)](pmma.md) | — | 1.18 g/cm³ | 70 MPa | 160 °C | 0.19 W/m·K | +| [Polyethylene](pe.md) | — | 0.94 g/cm³ | 25 MPa | 130 °C | 0.4 W/m·K | +| [HDPE (High-Density Polyethylene)](pe-hdpe.md) | — | 0.95 g/cm³ | 26 MPa | 130 °C | 0.4 W/m·K | +| [LDPE (Low-Density Polyethylene)](pe-ldpe.md) | — | 0.92 g/cm³ | 10 MPa | 110 °C | 0.4 W/m·K | +| [UHMWPE](pe-uhmwpe.md) | — | 0.93 g/cm³ | 20 MPa | 130 °C | 0.4 W/m·K | +| [Polycarbonate](pc.md) | — | 1.2 g/cm³ | 60 MPa | 267 °C | 0.2 W/m·K | diff --git a/docs/catalog/plastics/abs.md b/docs/catalog/plastics/abs.md new file mode 100644 index 0000000..1c347af --- /dev/null +++ b/docs/catalog/plastics/abs.md @@ -0,0 +1,28 @@ +# ABS (Acrylonitrile Butadiene Styrene) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.05 g/cm³ | +| Young's Modulus | 2.3 GPa | +| Yield Strength | 40 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 225 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.3, 0.3, 0.3, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/delrin.md b/docs/catalog/plastics/delrin.md new file mode 100644 index 0000000..215c66c --- /dev/null +++ b/docs/catalog/plastics/delrin.md @@ -0,0 +1,31 @@ +# Delrin (POM) + +## Identity + +| Field | Value | +|---|---| +| Formula | `-(CH2-O)n-` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.41 g/cm³ | +| Young's Modulus | 2.8 GPa | +| Yield Strength | 70 MPa | +| Tensile Strength | 70 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 165 °C | +| Thermal Conductivity | 0.25 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.85, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/esr.md b/docs/catalog/plastics/esr.md new file mode 100644 index 0000000..db5d3ff --- /dev/null +++ b/docs/catalog/plastics/esr.md @@ -0,0 +1,21 @@ +# ESR (3M Vikuiti) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.0 g/cm³ | +| Tensile Strength | 100 MPa | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.99, 0.99, 0.99, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.1 | diff --git a/docs/catalog/plastics/nylon.md b/docs/catalog/plastics/nylon.md new file mode 100644 index 0000000..db5c2fa --- /dev/null +++ b/docs/catalog/plastics/nylon.md @@ -0,0 +1,29 @@ +# Nylon 6 + +## Identity + +| Field | Value | +|---|---| +| Formula | `-(CH2)5-CO-NH-` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.13 g/cm³ | +| Young's Modulus | 2.8 GPa | +| Yield Strength | 45 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 215 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.85, 0.85, 0.8, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/pc.md b/docs/catalog/plastics/pc.md new file mode 100644 index 0000000..da9f3f7 --- /dev/null +++ b/docs/catalog/plastics/pc.md @@ -0,0 +1,34 @@ +# Polycarbonate + +## Identity + +| Field | Value | +|---|---| +| Formula | `C16H14O3` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.2 g/cm³ | +| Young's Modulus | 2.3 GPa | +| Yield Strength | 60 MPa | +| Tensile Strength | 65 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 267 °C | +| Thermal Conductivity | 0.2 W/(m·K) | +| Specific Heat | 1200 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.92, 0.92, 0.92, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.15 | +| IOR | 1.58 | +| Transmission | 0.85 | diff --git a/docs/catalog/plastics/pctfe.md b/docs/catalog/plastics/pctfe.md new file mode 100644 index 0000000..4f7d0ab --- /dev/null +++ b/docs/catalog/plastics/pctfe.md @@ -0,0 +1,27 @@ +# PCTFE (Polychlorotrifluoroethylene) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.13 g/cm³ | +| Yield Strength | 50 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 217 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.95, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.4 | diff --git a/docs/catalog/plastics/pe-hdpe.md b/docs/catalog/plastics/pe-hdpe.md new file mode 100644 index 0000000..1aa84cd --- /dev/null +++ b/docs/catalog/plastics/pe-hdpe.md @@ -0,0 +1,32 @@ +# HDPE (High-Density Polyethylene) + +## Identity + +| Field | Value | +|---|---| +| Grade | HDPE | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.95 g/cm³ | +| Young's Modulus | 1.1 GPa | +| Yield Strength | 26 MPa | +| Tensile Strength | 32 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 130 °C | +| Thermal Conductivity | 0.4 W/(m·K) | +| Specific Heat | 2300 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.88, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/pe-ldpe.md b/docs/catalog/plastics/pe-ldpe.md new file mode 100644 index 0000000..14c93a4 --- /dev/null +++ b/docs/catalog/plastics/pe-ldpe.md @@ -0,0 +1,32 @@ +# LDPE (Low-Density Polyethylene) + +## Identity + +| Field | Value | +|---|---| +| Grade | LDPE | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.92 g/cm³ | +| Young's Modulus | 0.2 GPa | +| Yield Strength | 10 MPa | +| Tensile Strength | 15 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 110 °C | +| Thermal Conductivity | 0.4 W/(m·K) | +| Specific Heat | 2300 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.88, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/pe-uhmwpe.md b/docs/catalog/plastics/pe-uhmwpe.md new file mode 100644 index 0000000..aaf005c --- /dev/null +++ b/docs/catalog/plastics/pe-uhmwpe.md @@ -0,0 +1,32 @@ +# UHMWPE + +## Identity + +| Field | Value | +|---|---| +| Grade | UHMWPE | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.93 g/cm³ | +| Young's Modulus | 0.8 GPa | +| Yield Strength | 20 MPa | +| Tensile Strength | 40 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 130 °C | +| Thermal Conductivity | 0.4 W/(m·K) | +| Specific Heat | 2300 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.88, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/pe.md b/docs/catalog/plastics/pe.md new file mode 100644 index 0000000..80eaa9e --- /dev/null +++ b/docs/catalog/plastics/pe.md @@ -0,0 +1,31 @@ +# Polyethylene + +## Identity + +| Field | Value | +|---|---| +| Formula | `-(CH2-CH2)n-` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 0.94 g/cm³ | +| Young's Modulus | 0.8 GPa | +| Yield Strength | 25 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 130 °C | +| Thermal Conductivity | 0.4 W/(m·K) | +| Specific Heat | 2300 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.88, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/peek-CF30.md b/docs/catalog/plastics/peek-CF30.md new file mode 100644 index 0000000..6102ab2 --- /dev/null +++ b/docs/catalog/plastics/peek-CF30.md @@ -0,0 +1,32 @@ +# PEEK 30% Carbon Filled + +## Identity + +| Field | Value | +|---|---| +| Grade | CF30 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.41 g/cm³ | +| Young's Modulus | 13 GPa | +| Yield Strength | 100 MPa | +| Tensile Strength | 100 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 334 °C | +| Thermal Conductivity | 0.25 W/(m·K) | +| Specific Heat | 1200 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.82, 0.7, 0.55, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/peek-GF30.md b/docs/catalog/plastics/peek-GF30.md new file mode 100644 index 0000000..a4daf3d --- /dev/null +++ b/docs/catalog/plastics/peek-GF30.md @@ -0,0 +1,32 @@ +# PEEK 30% Glass Filled + +## Identity + +| Field | Value | +|---|---| +| Grade | GF30 | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.53 g/cm³ | +| Young's Modulus | 10 GPa | +| Yield Strength | 170 MPa | +| Tensile Strength | 100 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 334 °C | +| Thermal Conductivity | 0.25 W/(m·K) | +| Specific Heat | 1200 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.82, 0.7, 0.55, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/peek-unfilled.md b/docs/catalog/plastics/peek-unfilled.md new file mode 100644 index 0000000..b5ab3bc --- /dev/null +++ b/docs/catalog/plastics/peek-unfilled.md @@ -0,0 +1,32 @@ +# PEEK Unfilled + +## Identity + +| Field | Value | +|---|---| +| Grade | unfilled | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.32 g/cm³ | +| Young's Modulus | 3.6 GPa | +| Yield Strength | 100 MPa | +| Tensile Strength | 100 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 334 °C | +| Thermal Conductivity | 0.25 W/(m·K) | +| Specific Heat | 1200 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.82, 0.7, 0.55, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/peek-victrex.md b/docs/catalog/plastics/peek-victrex.md new file mode 100644 index 0000000..d533612 --- /dev/null +++ b/docs/catalog/plastics/peek-victrex.md @@ -0,0 +1,31 @@ +# Victrex PEEK + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.32 g/cm³ | +| Young's Modulus | 3.6 GPa | +| Yield Strength | 100 MPa | +| Tensile Strength | 100 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 334 °C | +| Thermal Conductivity | 0.25 W/(m·K) | +| Specific Heat | 1200 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.82, 0.7, 0.55, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/peek.md b/docs/catalog/plastics/peek.md new file mode 100644 index 0000000..9494d5e --- /dev/null +++ b/docs/catalog/plastics/peek.md @@ -0,0 +1,32 @@ +# PEEK + +## Identity + +| Field | Value | +|---|---| +| Formula | `C19H12O3` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.32 g/cm³ | +| Young's Modulus | 3.6 GPa | +| Yield Strength | 100 MPa | +| Tensile Strength | 100 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 334 °C | +| Thermal Conductivity | 0.25 W/(m·K) | +| Specific Heat | 1200 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.82, 0.7, 0.55, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/petg.md b/docs/catalog/plastics/petg.md new file mode 100644 index 0000000..bda57ca --- /dev/null +++ b/docs/catalog/plastics/petg.md @@ -0,0 +1,27 @@ +# PETG + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.27 g/cm³ | +| Young's Modulus | 2.3 GPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 225 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.85, 0.85, 0.8, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/pla.md b/docs/catalog/plastics/pla.md new file mode 100644 index 0000000..9401c44 --- /dev/null +++ b/docs/catalog/plastics/pla.md @@ -0,0 +1,29 @@ +# PLA (Polylactic Acid) + +## Identity + +| Field | Value | +|---|---| +| Formula | `C3H4O2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.25 g/cm³ | +| Young's Modulus | 2.7 GPa | +| Yield Strength | 50 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 160 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.8, 0.8, 0.8, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.6 | diff --git a/docs/catalog/plastics/pmma.md b/docs/catalog/plastics/pmma.md new file mode 100644 index 0000000..d0f5c26 --- /dev/null +++ b/docs/catalog/plastics/pmma.md @@ -0,0 +1,34 @@ +# PMMA (Acrylic) + +## Identity + +| Field | Value | +|---|---| +| Formula | `C5H8O2` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.18 g/cm³ | +| Young's Modulus | 3.0 GPa | +| Yield Strength | 70 MPa | +| Tensile Strength | 75 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 160 °C | +| Thermal Conductivity | 0.19 W/(m·K) | +| Specific Heat | 1470 J/(kg·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 0.95, 0.9)` | +| Metallic | 0.0 | +| Roughness | 0.1 | +| IOR | 1.49 | +| Transmission | 0.9 | diff --git a/docs/catalog/plastics/ptfe-reflector.md b/docs/catalog/plastics/ptfe-reflector.md new file mode 100644 index 0000000..d8e08a3 --- /dev/null +++ b/docs/catalog/plastics/ptfe-reflector.md @@ -0,0 +1,29 @@ +# PTFE Reflector Tape + +## Identity + +| Field | Value | +|---|---| +| Treatment | reflector | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.15 g/cm³ | +| Yield Strength | 20 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 327 °C | +| Thermal Conductivity | 0.24 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.98, 0.98, 0.98, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.2 | diff --git a/docs/catalog/plastics/ptfe.md b/docs/catalog/plastics/ptfe.md new file mode 100644 index 0000000..2cbc805 --- /dev/null +++ b/docs/catalog/plastics/ptfe.md @@ -0,0 +1,29 @@ +# PTFE (Teflon) + +## Identity + +| Field | Value | +|---|---| +| Formula | `-(CF2-CF2)n-` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 2.15 g/cm³ | +| Yield Strength | 20 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 327 °C | +| Thermal Conductivity | 0.24 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.95, 0.95, 0.95, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.3 | diff --git a/docs/catalog/plastics/torlon.md b/docs/catalog/plastics/torlon.md new file mode 100644 index 0000000..a1f978c --- /dev/null +++ b/docs/catalog/plastics/torlon.md @@ -0,0 +1,28 @@ +# Torlon (PAI) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.45 g/cm³ | +| Young's Modulus | 5.2 GPa | +| Yield Strength | 110 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 330 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.85, 0.6, 0.3, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/tpu.md b/docs/catalog/plastics/tpu.md new file mode 100644 index 0000000..61bd26d --- /dev/null +++ b/docs/catalog/plastics/tpu.md @@ -0,0 +1,20 @@ +# TPU (Thermoplastic Polyurethane) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.21 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.6, 0.6, 0.6, 0.8)` | +| Metallic | 0.0 | +| Roughness | 0.7 | diff --git a/docs/catalog/plastics/ultem.md b/docs/catalog/plastics/ultem.md new file mode 100644 index 0000000..2c60558 --- /dev/null +++ b/docs/catalog/plastics/ultem.md @@ -0,0 +1,30 @@ +# Ultem (PEI) + +## Identity + +| Field | Value | +|---|---| +| Formula | `C37H24N2O6` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.27 g/cm³ | +| Young's Modulus | 3.6 GPa | +| Yield Strength | 90 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 340 °C | +| Thermal Conductivity | 0.22 W/(m·K) | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.85, 0.65, 0.2, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/plastics/vespel.md b/docs/catalog/plastics/vespel.md new file mode 100644 index 0000000..99fc187 --- /dev/null +++ b/docs/catalog/plastics/vespel.md @@ -0,0 +1,28 @@ +# Vespel (Polyimide) + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.42 g/cm³ | +| Young's Modulus | 3.8 GPa | +| Yield Strength | 70 MPa | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 400 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.4, 0.2, 0.1, 1.0)` | +| Metallic | 0.0 | +| Roughness | 0.5 | diff --git a/docs/catalog/scintillators/README.md b/docs/catalog/scintillators/README.md new file mode 100644 index 0000000..1e5f72a --- /dev/null +++ b/docs/catalog/scintillators/README.md @@ -0,0 +1,19 @@ +# Scintillators + +13 materials. Click a name for full properties. + +| Material | Preview | Density | n (IOR) | +|---|---|---|---| +| [LYSO](lyso.md) | — | 7.1 g/cm³ | 1.82 | +| [LYSO:Ce](lyso-Ce.md) | — | 7.1 g/cm³ | 1.82 | +| [BGO](bgo.md) | — | 7.13 g/cm³ | 2.15 | +| [NaI](nai.md) | — | 3.67 g/cm³ | 1.85 | +| [NaI(Tl)](nai-Tl.md) | — | 3.67 g/cm³ | 1.85 | +| [CsI](csi.md) | — | 4.51 g/cm³ | 1.95 | +| [CsI(Tl)](csi-Tl.md) | — | 4.51 g/cm³ | 1.95 | +| [CsI(Na)](csi-Na.md) | — | 4.51 g/cm³ | 1.95 | +| [LaBr3:Ce](labr3.md) | — | 5.29 g/cm³ | 1.9 | +| [PWO](pwo.md) | — | 8.28 g/cm³ | 2.26 | +| [Plastic Scintillator](plastic_scint.md) | — | 1.032 g/cm³ | — | +| [BC-400 Plastic Scintillator](plastic_scint-BC400.md) | — | 1.032 g/cm³ | — | +| [EJ-200 Plastic Scintillator](plastic_scint-EJ200.md) | — | 1.032 g/cm³ | — | diff --git a/docs/catalog/scintillators/bgo.md b/docs/catalog/scintillators/bgo.md new file mode 100644 index 0000000..4a25782 --- /dev/null +++ b/docs/catalog/scintillators/bgo.md @@ -0,0 +1,29 @@ +# BGO + +## Identity + +| Field | Value | +|---|---| +| Formula | `Bi4Ge3O12` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 7.13 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 1050 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.58, 0.44, 0.86, 0.8)` | +| Metallic | 0.0 | +| Roughness | 0.3 | +| IOR | 2.15 | +| Transmission | 0.75 | diff --git a/docs/catalog/scintillators/csi-Na.md b/docs/catalog/scintillators/csi-Na.md new file mode 100644 index 0000000..921605d --- /dev/null +++ b/docs/catalog/scintillators/csi-Na.md @@ -0,0 +1,23 @@ +# CsI(Na) + +## Identity + +| Field | Value | +|---|---| +| Formula | `CsI:Na` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 4.51 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.7, 0.75)` | +| Metallic | 0.0 | +| Roughness | 0.25 | +| IOR | 1.95 | +| Transmission | 0.8 | diff --git a/docs/catalog/scintillators/csi-Tl.md b/docs/catalog/scintillators/csi-Tl.md new file mode 100644 index 0000000..f0a7782 --- /dev/null +++ b/docs/catalog/scintillators/csi-Tl.md @@ -0,0 +1,23 @@ +# CsI(Tl) + +## Identity + +| Field | Value | +|---|---| +| Formula | `CsI:Tl` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 4.51 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.7, 0.75)` | +| Metallic | 0.0 | +| Roughness | 0.25 | +| IOR | 1.95 | +| Transmission | 0.8 | diff --git a/docs/catalog/scintillators/csi.md b/docs/catalog/scintillators/csi.md new file mode 100644 index 0000000..f35f5dd --- /dev/null +++ b/docs/catalog/scintillators/csi.md @@ -0,0 +1,23 @@ +# CsI + +## Identity + +| Field | Value | +|---|---| +| Formula | `CsI` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 4.51 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.7, 0.75)` | +| Metallic | 0.0 | +| Roughness | 0.25 | +| IOR | 1.95 | +| Transmission | 0.8 | diff --git a/docs/catalog/scintillators/labr3.md b/docs/catalog/scintillators/labr3.md new file mode 100644 index 0000000..90ba806 --- /dev/null +++ b/docs/catalog/scintillators/labr3.md @@ -0,0 +1,23 @@ +# LaBr3:Ce + +## Identity + +| Field | Value | +|---|---| +| Formula | `LaBr3:Ce` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 5.29 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.8, 0.7, 0.9, 0.8)` | +| Metallic | 0.0 | +| Roughness | 0.3 | +| IOR | 1.9 | +| Transmission | 0.8 | diff --git a/docs/catalog/scintillators/lyso-Ce.md b/docs/catalog/scintillators/lyso-Ce.md new file mode 100644 index 0000000..990ac8d --- /dev/null +++ b/docs/catalog/scintillators/lyso-Ce.md @@ -0,0 +1,29 @@ +# LYSO:Ce + +## Identity + +| Field | Value | +|---|---| +| Formula | `Lu1.8Y0.2SiO5:Ce` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 7.1 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 2050 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.0, 1.0, 1.0, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.3 | +| IOR | 1.82 | +| Transmission | 0.8 | diff --git a/docs/catalog/scintillators/lyso.md b/docs/catalog/scintillators/lyso.md new file mode 100644 index 0000000..85809b9 --- /dev/null +++ b/docs/catalog/scintillators/lyso.md @@ -0,0 +1,29 @@ +# LYSO + +## Identity + +| Field | Value | +|---|---| +| Formula | `Lu1.8Y0.2SiO5` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 7.1 g/cm³ | + +## Thermal Properties + +| Property | Value | +|---|---| +| Melting Point | 2050 °C | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.0, 1.0, 1.0, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.3 | +| IOR | 1.82 | +| Transmission | 0.8 | diff --git a/docs/catalog/scintillators/nai-Tl.md b/docs/catalog/scintillators/nai-Tl.md new file mode 100644 index 0000000..91b0102 --- /dev/null +++ b/docs/catalog/scintillators/nai-Tl.md @@ -0,0 +1,23 @@ +# NaI(Tl) + +## Identity + +| Field | Value | +|---|---| +| Formula | `NaI:Tl` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 3.67 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(1.0, 0.9, 0.7, 0.7)` | +| Metallic | 0.0 | +| Roughness | 0.2 | +| IOR | 1.85 | +| Transmission | 0.85 | diff --git a/docs/catalog/scintillators/nai.md b/docs/catalog/scintillators/nai.md new file mode 100644 index 0000000..df7ff51 --- /dev/null +++ b/docs/catalog/scintillators/nai.md @@ -0,0 +1,23 @@ +# NaI + +## Identity + +| Field | Value | +|---|---| +| Formula | `NaI` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 3.67 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(1.0, 0.9, 0.7, 0.7)` | +| Metallic | 0.0 | +| Roughness | 0.2 | +| IOR | 1.85 | +| Transmission | 0.85 | diff --git a/docs/catalog/scintillators/plastic_scint-BC400.md b/docs/catalog/scintillators/plastic_scint-BC400.md new file mode 100644 index 0000000..5bb70f6 --- /dev/null +++ b/docs/catalog/scintillators/plastic_scint-BC400.md @@ -0,0 +1,21 @@ +# BC-400 Plastic Scintillator + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.032 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.85, 0.9)` | +| Metallic | 0.0 | +| Roughness | 0.4 | +| Transmission | 0.85 | diff --git a/docs/catalog/scintillators/plastic_scint-EJ200.md b/docs/catalog/scintillators/plastic_scint-EJ200.md new file mode 100644 index 0000000..91ba9c8 --- /dev/null +++ b/docs/catalog/scintillators/plastic_scint-EJ200.md @@ -0,0 +1,21 @@ +# EJ-200 Plastic Scintillator + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.032 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.85, 0.9)` | +| Metallic | 0.0 | +| Roughness | 0.4 | +| Transmission | 0.85 | diff --git a/docs/catalog/scintillators/plastic_scint.md b/docs/catalog/scintillators/plastic_scint.md new file mode 100644 index 0000000..01d47b3 --- /dev/null +++ b/docs/catalog/scintillators/plastic_scint.md @@ -0,0 +1,21 @@ +# Plastic Scintillator + +## Identity + +| Field | Value | +|---|---| + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 1.032 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.9, 0.9, 0.85, 0.9)` | +| Metallic | 0.0 | +| Roughness | 0.4 | +| Transmission | 0.85 | diff --git a/docs/catalog/scintillators/pwo.md b/docs/catalog/scintillators/pwo.md new file mode 100644 index 0000000..475257c --- /dev/null +++ b/docs/catalog/scintillators/pwo.md @@ -0,0 +1,23 @@ +# PWO + +## Identity + +| Field | Value | +|---|---| +| Formula | `PbWO4` | + +## Mechanical Properties + +| Property | Value | +|---|---| +| Density | 8.28 g/cm³ | + +## PBR (Rendering) + +| Property | Value | +|---|---| +| Base Color | `(0.4, 0.4, 0.45, 0.85)` | +| Metallic | 0.0 | +| Roughness | 0.35 | +| IOR | 2.26 | +| Transmission | 0.7 | diff --git a/pyproject.toml b/pyproject.toml index c65abc5..bb3fd75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,27 +29,18 @@ classifiers = [ dependencies = [ "pint>=0.20", + "periodictable>=1.6.0", + "uncertainties>=3.2", + "mat-vis-client>=2026.4.0", "tomli>=1.0.0; python_version == '3.10'", ] [project.optional-dependencies] -periodictable = ["periodictable>=1.6.0"] -matproj = ["pymatgen>=2024.0.0"] -# build123d pulls cadquery-ocp and vtk, which only publish wheels for -# cp311/cp312 as of 2026-04. The environment marker means `pip install -# py-materials[build123d]` on Python 3.13+ is a no-op rather than a hard -# wheel-resolution error — installers silently drop the dep. See #11. -build123d = ["build123d>=0.7.0; python_version<'3.13'"] dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "build123d>=0.7.0; python_version<'3.13'", ] -all = [ - "periodictable>=1.6.0", - "pymatgen>=2024.0.0", - "build123d>=0.7.0; python_version<'3.13'", -] [project.urls] Homepage = "https://github.com/MorePet/py-mat" @@ -72,6 +63,11 @@ testpaths = ["tests"] python_files = ["test_*.py"] python_functions = ["test_*"] addopts = "-v" +filterwarnings = [ + "ignore:Material\\(pbr=:DeprecationWarning", + "ignore:TOML \\[pbr\\] section:DeprecationWarning", + "ignore:AffineScalarFunc:FutureWarning", +] [tool.ruff] line-length = 100 diff --git a/scripts/enrich_vis.py b/scripts/enrich_vis.py new file mode 100644 index 0000000..01408de --- /dev/null +++ b/scripts/enrich_vis.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Propose [vis] mappings for materials that don't have one. + +Uses tag-based matching against the mat-vis index — the ambientcg +and polyhaven tags ("brushed", "silver", "oak", "concrete", etc.) +give far better signal than category alone. + +Usage: + python scripts/enrich_vis.py # preview + python scripts/enrich_vis.py -o proposed.toml +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) + +from pymat import load_all, vis + + +# Per-material tag heuristics — richer than category-only matching +# Each tuple: (category, required_tags_to_try_in_order) +# Multiple tag sets are tried; first non-empty result wins. +MATERIAL_HINTS: dict[str, tuple[str, list[list[str]]]] = { + # Metals — finish matters + "stainless": ("metal", [["brushed", "silver", "steel"], ["silver", "steel"], ["metal"]]), + "s304": ("metal", [["brushed", "silver", "steel"], ["silver", "steel"]]), + "s316L": ("metal", [["brushed", "silver", "steel"], ["silver", "steel"]]), + "s303": ("metal", [["brushed", "silver", "steel"], ["silver", "steel"]]), + "s17_4PH": ("metal", [["brushed", "silver", "steel"], ["silver", "steel"]]), + "electropolished": ("metal", [["clean", "silver", "smooth"], ["smooth", "silver"]]), + "passivated": ("metal", [["brushed", "silver"], ["silver", "steel"]]), + "aluminum": ("metal", [["clean", "silver"], ["silver", "metal"]]), + "a6061": ("metal", [["clean", "silver"], ["silver"]]), + "a7075": ("metal", [["clean", "silver"], ["silver"]]), + "a2024": ("metal", [["clean", "silver"], ["silver"]]), + "a6063": ("metal", [["clean", "silver"], ["silver"]]), + "copper": ("metal", [["copper", "clean", "shiny"], ["copper"]]), + "OFHC": ("metal", [["copper", "clean"], ["copper"]]), + "brass": ("metal", [["brass", "gold"], ["bronze"], ["copper", "gold"]]), + "tungsten": ("metal", [["iron"], ["metal"]]), + "pure": ("metal", [["iron"], ["metal"]]), + "W90": ("metal", [["iron"], ["metal"]]), + "lead": ("metal", [["grey", "metal", "smooth"], ["metal"]]), + "titanium": ("metal", [["clean", "silver"], ["silver"]]), + "grade5": ("metal", [["clean", "silver"], ["silver"]]), + # Plastics — mostly matte, colored + "peek": ("plastic", [["plastic"]]), + "delrin": ("plastic", [["plastic"]]), + "nylon": ("plastic", [["plastic"]]), + "pla": ("plastic", [["plastic"]]), + "abs": ("plastic", [["plastic"]]), + "petg": ("plastic", [["plastic"]]), + "ptfe": ("plastic", [["white", "plastic"], ["plastic"]]), + "pmma": ("plastic", [["plastic"]]), + "pe": ("plastic", [["plastic"]]), + "pc": ("plastic", [["plastic"]]), + "ultem": ("plastic", [["plastic"]]), + "torlon": ("plastic", [["plastic"]]), + "vespel": ("plastic", [["plastic"]]), + "tpu": ("plastic", [["plastic"]]), + "pctfe": ("plastic", [["plastic"]]), + "esr": ("plastic", [["white"], ["plastic"]]), + # Ceramics + "alumina": ("ceramic", [["white", "clean"], ["ceramic"]]), + "macor": ("ceramic", [["white"], ["ceramic"]]), + "zirconia": ("ceramic", [["white"], ["ceramic"]]), + "glass": ("ceramic", [["glass"]]), # fallback since glass is rare +} + + +def _hints_for(key: str, name: str) -> tuple[str | None, list[list[str]]]: + """Return (category, list of tag sets to try in order).""" + key_lower = key.lower() + name_lower = name.lower() + + for k, (cat, tag_sets) in MATERIAL_HINTS.items(): + if k.lower() in key_lower or k.lower() in name_lower: + return cat, tag_sets + + # Generic category hints as fallback + if any(w in name_lower for w in ["steel", "iron", "alloy"]): + return "metal", [["silver", "steel"], ["metal"]] + if any(w in name_lower for w in ["wood", "ply", "mdf"]): + return "wood", [["wood"]] + if "concrete" in name_lower: + return "concrete", [["concrete"]] + if any(w in name_lower for w in ["stone", "rock", "marble"]): + return "stone", [["stone"]] + + return None, [[]] + + +def propose_mappings(limit_per_material: int = 3) -> list[dict]: + """Generate vis mapping proposals using tag-based matching.""" + materials = load_all() + proposals = [] + + for key, mat in materials.items(): + if mat.vis.source_id is not None: + continue + + category, tag_sets = _hints_for(key, mat.name) + if not category: + continue + + # Try tag sets in order, first match wins + candidates = [] + tags_used = None + for tags in tag_sets: + try: + results = vis.search( + category=category, + tags=tags if tags else None, + limit=limit_per_material, + ) + except ConnectionError: + continue + if results: + candidates = results + tags_used = tags + break + + if not candidates: + continue + + # Format source_id as "source/id" + for c in candidates: + if "source" in c and "id" in c and "/" not in c["id"]: + c["id"] = f"{c['source']}/{c['id']}" + + proposals.append({ + "material_key": key, + "material_name": mat.name, + "category": category, + "tags_matched": tags_used, + "candidates": candidates, + }) + + return proposals + + +def format_toml(proposals: list[dict]) -> str: + """Format proposals as TOML [vis] sections.""" + lines = ["# Auto-generated vis mapping proposals (tag-based matching)", ""] + + for p in proposals: + key = p["material_key"] + top = p["candidates"][0] + alts = p["candidates"][1:] + + lines.append(f"# {p['material_name']} — matched on tags {p['tags_matched']}") + lines.append(f"# top tags: {', '.join(top.get('tags', [])[:6])}") + if alts: + lines.append(f"# alternatives: {[c['id'] for c in alts]}") + lines.append(f"[{key}.vis.finishes]") + lines.append(f'default = "{top["id"]}"') + lines.append("") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", "-o", help="Write proposals to file") + args = parser.parse_args() + + proposals = propose_mappings() + + if not proposals: + print("No unmapped materials found (or mat-vis index unavailable)") + return + + toml_text = format_toml(proposals) + + if args.output: + Path(args.output).write_text(toml_text) + print(f"Wrote {len(proposals)} proposals to {args.output}", file=sys.stderr) + else: + print(toml_text) + print(f"\n# {len(proposals)} materials proposed", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_catalog.py b/scripts/generate_catalog.py new file mode 100644 index 0000000..ae72ea4 --- /dev/null +++ b/scripts/generate_catalog.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +"""Generate a markdown material catalog with thumbnails from mat-vis. + +Outputs a docs/catalog/ tree with per-category index pages and +per-material detail pages, each with a small thumbnail PNG from +the mat-vis color texture. + +Usage: + python scripts/generate_catalog.py # generate to docs/catalog/ + python scripts/generate_catalog.py --output /tmp/cat # custom output dir + python scripts/generate_catalog.py --skip-thumbnails # text only, no vis fetch + +Called by .github/workflows/catalog.yml on push to dev. +""" + +from __future__ import annotations + +import argparse +import logging +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) + +from pymat import load_all + +log = logging.getLogger("catalog") + +THUMB_TIER = "128" # mat-vis hosts 128/256/512 thumbnail tiers +CATEGORIES_ORDER = [ + "metals", "scintillators", "ceramics", "plastics", + "electronics", "liquids", "gases", +] + + +def _fetch_thumbnail(source: str, material_id: str) -> bytes: + """Fetch a thumbnail PNG from mat-vis's thumbnail tier. + + mat-vis hosts 128/256/512 tiers as pre-baked small textures — + no Pillow resize needed, just fetch the color channel at the + thumbnail tier. + """ + try: + from pymat import vis + textures = vis.fetch(source, material_id, tier=THUMB_TIER) + return textures.get("color", b"") + except Exception as exc: + log.debug("thumbnail fetch failed for %s/%s: %s", source, material_id, exc) + return b"" + + +def _format_value(key: str, value, unit: str | None = None) -> str: + """Format a property value for markdown.""" + if value is None: + return "—" + if isinstance(value, float): + formatted = f"{value:.4g}" + else: + formatted = str(value) + if unit: + formatted += f" {unit}" + return formatted + + +def _material_page(mat, thumb_path: str | None, category: str) -> str: + """Generate markdown for a single material detail page.""" + lines = [f"# {mat.name}", ""] + + if thumb_path: + lines.append(f"![{mat.name}]({thumb_path})") + lines.append("") + + # Identity + lines.append("## Identity") + lines.append("") + lines.append(f"| Field | Value |") + lines.append(f"|---|---|") + if mat.formula: + lines.append(f"| Formula | `{mat.formula}` |") + if mat.grade: + lines.append(f"| Grade | {mat.grade} |") + if mat.temper: + lines.append(f"| Temper | {mat.temper} |") + if mat.treatment: + lines.append(f"| Treatment | {mat.treatment} |") + lines.append("") + + # Mechanical + mech = mat.properties.mechanical + if mech.density is not None or mech.youngs_modulus is not None: + lines.append("## Mechanical Properties") + lines.append("") + lines.append("| Property | Value |") + lines.append("|---|---|") + if mech.density is not None: + lines.append(f"| Density | {mech.density} g/cm³ |") + if mech.youngs_modulus is not None: + lines.append(f"| Young's Modulus | {mech.youngs_modulus} GPa |") + if mech.yield_strength is not None: + lines.append(f"| Yield Strength | {mech.yield_strength} MPa |") + if mech.tensile_strength is not None: + lines.append(f"| Tensile Strength | {mech.tensile_strength} MPa |") + if mech.poissons_ratio is not None: + lines.append(f"| Poisson's Ratio | {mech.poissons_ratio} |") + if mech.hardness_vickers is not None: + lines.append(f"| Hardness (Vickers) | {mech.hardness_vickers} |") + lines.append("") + + # Thermal + therm = mat.properties.thermal + if therm.melting_point is not None: + lines.append("## Thermal Properties") + lines.append("") + lines.append("| Property | Value |") + lines.append("|---|---|") + if therm.melting_point is not None: + lines.append(f"| Melting Point | {therm.melting_point} °C |") + if therm.thermal_conductivity is not None: + lines.append(f"| Thermal Conductivity | {therm.thermal_conductivity} W/(m·K) |") + if therm.specific_heat is not None: + lines.append(f"| Specific Heat | {therm.specific_heat} J/(kg·K) |") + lines.append("") + + # PBR + pbr = mat.properties.pbr + lines.append("## PBR (Rendering)") + lines.append("") + lines.append("| Property | Value |") + lines.append("|---|---|") + lines.append(f"| Base Color | `{pbr.base_color}` |") + lines.append(f"| Metallic | {pbr.metallic} |") + lines.append(f"| Roughness | {pbr.roughness} |") + if pbr.ior != 1.5: + lines.append(f"| IOR | {pbr.ior} |") + if pbr.transmission > 0: + lines.append(f"| Transmission | {pbr.transmission} |") + lines.append("") + + # Vis + if mat.vis.source_id: + lines.append("## Visual (mat-vis)") + lines.append("") + lines.append(f"| Field | Value |") + lines.append(f"|---|---|") + lines.append(f"| Source ID | `{mat.vis.source_id}` |") + if mat.vis.finish: + lines.append(f"| Finish | {mat.vis.finish} |") + if mat.vis.finishes: + finishes = ", ".join(mat.vis.finishes.keys()) + lines.append(f"| Available Finishes | {finishes} |") + lines.append("") + + # Composition + if mat.composition: + lines.append("## Composition") + lines.append("") + lines.append("| Element | Fraction |") + lines.append("|---|---|") + for el, frac in sorted( + mat.composition.items(), + key=lambda x: -(getattr(x[1], "nominal_value", x[1])), + ): + nominal = getattr(frac, "nominal_value", frac) + stddev = getattr(frac, "std_dev", None) + if stddev and stddev > 0: + lines.append(f"| {el} | {nominal:.4g} ± {stddev:.4g} |") + else: + lines.append(f"| {el} | {nominal:.4g} |") + lines.append("") + + return "\n".join(lines) + + +def _fmt_density(mat) -> str: + d = mat.properties.mechanical.density + return f"{d} g/cm³" if d else "—" + + +def _fmt_yield(mat) -> str: + y = mat.properties.mechanical.yield_strength + return f"{y} MPa" if y else "—" + + +def _fmt_tensile(mat) -> str: + t = mat.properties.mechanical.tensile_strength + return f"{t} MPa" if t else "—" + + +def _fmt_modulus(mat) -> str: + e = mat.properties.mechanical.youngs_modulus + return f"{e} GPa" if e else "—" + + +def _fmt_melting(mat) -> str: + mp = mat.properties.thermal.melting_point + return f"{mp} °C" if mp is not None else "—" + + +def _fmt_k(mat) -> str: + k = mat.properties.thermal.thermal_conductivity + return f"{k} W/m·K" if k else "—" + + +def _fmt_ior(mat) -> str: + n = mat.properties.optical.refractive_index + return f"{n}" if n else "—" + + +# Per-category columns chosen by audience +# Metals: mechanical + thermal (engineering primary) +# Plastics: density + thermal (operating temp matters) +# Scintillators: density + optical (physics primary) +# Gases/liquids: density + thermal +# Default: density + mechanical +_CATEGORY_COLUMNS: dict[str, list[tuple[str, callable]]] = { + "metals": [ + ("Density", _fmt_density), + ("Yield", _fmt_yield), + ("Tensile", _fmt_tensile), + ("E", _fmt_modulus), + ("T_melt", _fmt_melting), + ], + "plastics": [ + ("Density", _fmt_density), + ("Yield", _fmt_yield), + ("T_melt", _fmt_melting), + ("k", _fmt_k), + ], + "scintillators": [ + ("Density", _fmt_density), + ("n (IOR)", _fmt_ior), + ], + "ceramics": [ + ("Density", _fmt_density), + ("E", _fmt_modulus), + ("T_melt", _fmt_melting), + ], + "electronics": [ + ("Density", _fmt_density), + ], + "liquids": [ + ("Density", _fmt_density), + ("n (IOR)", _fmt_ior), + ], + "gases": [ + ("Density", _fmt_density), + ], +} + + +def _category_index(category: str, materials: list, has_thumbnails: bool) -> str: + """Generate markdown index for a category — columns tuned per audience.""" + lines = [f"# {category.title()}", ""] + lines.append(f"{len(materials)} materials. Click a name for full properties.") + lines.append("") + + cols = _CATEGORY_COLUMNS.get(category, [("Density", _fmt_density)]) + + # Header + header = ["Material"] + if has_thumbnails: + header.append("Preview") + header.extend(c[0] for c in cols) + lines.append("| " + " | ".join(header) + " |") + lines.append("|" + "|".join(["---"] * len(header)) + "|") + + # Rows + for mat, key in materials: + row = [f"[{mat.name}]({key}.md)"] + if has_thumbnails: + if (THUMBS_EXIST := (mat.vis.source_id is not None)): + row.append(f"![]({'thumbs/' + key + '.png'})") + else: + row.append("—") + for _, fmt in cols: + row.append(fmt(mat)) + lines.append("| " + " | ".join(row) + " |") + + lines.append("") + return "\n".join(lines) + + +def _root_index(categories: dict[str, list]) -> str: + """Generate root README with links to categories.""" + lines = ["# Material Catalog", ""] + lines.append("Auto-generated from py-mat TOML data + mat-vis textures.") + lines.append("") + lines.append("| Category | Materials |") + lines.append("|---|---|") + for cat, mats in categories.items(): + lines.append(f"| [{cat.title()}]({cat}/README.md) | {len(mats)} |") + lines.append("") + return "\n".join(lines) + + +def generate(output_dir: Path, skip_thumbnails: bool = False) -> None: + """Generate the full catalog.""" + output_dir.mkdir(parents=True, exist_ok=True) + all_materials = load_all() + + # Group by category (from the TOML key hierarchy) + from pymat import _CATEGORY_BASES + categories: dict[str, list] = {} + + for category, base_keys in _CATEGORY_BASES.items(): + mats_in_cat = [] + for key in base_keys: + mat = all_materials.get(key) + if mat: + mats_in_cat.append((mat, key)) + # Also add children + for child_key, child_mat in mat._children.items(): + mats_in_cat.append((child_mat, f"{key}-{child_key}")) + if mats_in_cat: + categories[category] = mats_in_cat + + # Fetch thumbnails from mat-vis's thumbnail tier (128px, pre-baked) + thumb_count = 0 + if not skip_thumbnails: + for category, mats in categories.items(): + thumb_dir = output_dir / category / "thumbs" + thumb_dir.mkdir(parents=True, exist_ok=True) + for mat, key in mats: + if not mat.vis.source_id: + continue + thumb_path = thumb_dir / f"{key}.png" + if thumb_path.exists(): + thumb_count += 1 + continue + parts = mat.vis.source_id.split("/", 1) + if len(parts) != 2: + continue + source, material_id = parts + thumb_bytes = _fetch_thumbnail(source, material_id) + if thumb_bytes: + thumb_path.write_bytes(thumb_bytes) + thumb_count += 1 + log.info("thumbnail: %s", key) + + has_thumbnails = thumb_count > 0 + + # Generate pages + for category, mats in categories.items(): + cat_dir = output_dir / category + cat_dir.mkdir(parents=True, exist_ok=True) + + # Category index + (cat_dir / "README.md").write_text(_category_index(category, mats, has_thumbnails)) + + # Per-material pages + for mat, key in mats: + thumb_rel = f"thumbs/{key}.png" if (cat_dir / "thumbs" / f"{key}.png").exists() else None + page = _material_page(mat, thumb_rel, category) + (cat_dir / f"{key}.md").write_text(page) + + # Root index + (output_dir / "README.md").write_text(_root_index(categories)) + + total_mats = sum(len(m) for m in categories.values()) + print(f"Catalog: {total_mats} materials, {len(categories)} categories, {thumb_count} thumbnails") + print(f"Output: {output_dir}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", "-o", default="docs/catalog", help="Output directory") + parser.add_argument("--skip-thumbnails", action="store_true", help="Skip vis texture fetch") + parser.add_argument("-v", "--verbose", action="store_true") + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + generate(Path(args.output), skip_thumbnails=args.skip_thumbnails) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index ed7a42f..06c5aa8 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -13,33 +13,70 @@ """ import re +import textwrap from pathlib import Path from typing import List, Tuple def extract_docstring(source: str, func_name: str) -> str: - """Extract the docstring from a test function.""" - # Find the function definition + """Extract the docstring from a test function, dedented.""" pattern = rf"def {func_name}\(.*?\):\n\s+\"\"\"(.*?)\"\"\"" match = re.search(pattern, source, re.DOTALL) - if match: - return match.group(1).strip() - return "" + if not match: + return "" + raw = match.group(1) + # Drop the leading newline after `"""` so dedent can see a uniform + # indent on all remaining lines, then strip any trailing whitespace. + if raw.startswith("\n"): + raw = raw[1:] + return textwrap.dedent(raw).rstrip() def extract_code_block(source: str, func_name: str) -> str: - """Extract the code block after the docstring from a test function.""" - # Find the function definition + """ + Extract the code block after the docstring from a test function. + + Returns the code dedented to column 0 with assertion / importorskip + scaffolding removed, so it renders as a plain, runnable Python + snippet in the README. + """ pattern = rf"def {func_name}\(.*?\):\n\s+\"\"\".*?\"\"\"\n(.*?)(?=\n def |\nclass |\Z)" match = re.search(pattern, source, re.DOTALL) - if match: - code = match.group(1).strip() - # Remove pytest.importorskip lines - code = re.sub(r"\s*pytest\.importorskip\(.*?\)\n", "", code) - # Remove assert statements - code = re.sub(r"\s*assert .*?\n", "", code) - return code.strip() - return "" + if not match: + return "" + + raw = match.group(1).rstrip() + if not raw.strip(): + return "" + + # Dedent the entire block relative to its own leading indent. Test + # function bodies are indented 8 spaces (4 for the class, 4 for the + # method); `textwrap.dedent` handles arbitrary depths correctly. + dedented = textwrap.dedent(raw) + + # Filter out pytest scaffolding that doesn't belong in a user-facing + # example. Keep comments, blank lines, imports, asserts (they show + # the expected return values), and actual code. + kept: list[str] = [] + for line in dedented.splitlines(): + stripped = line.strip() + if stripped.startswith("pytest.importorskip"): + continue + kept.append(line) + + # Collapse runs of blank lines left behind by filtering. + cleaned: list[str] = [] + prev_blank = False + for line in kept: + if not line.strip(): + if prev_blank: + continue + prev_blank = True + else: + prev_blank = False + cleaned.append(line) + + return "\n".join(cleaned).strip() def parse_test_file(test_file_path: Path) -> List[Tuple[str, str, str]]: diff --git a/src/pymat/__init__.py b/src/pymat/__init__.py index 12d4c4b..dc8b53b 100644 --- a/src/pymat/__init__.py +++ b/src/pymat/__init__.py @@ -40,7 +40,7 @@ from .core import Material # Exports -from . import factories, registry +from . import factories, registry, vis from .core import Material from .enrichers import enrich_all, enrich_from_matproj, enrich_from_periodictable from .loader import load_category, load_toml @@ -76,6 +76,7 @@ "enrich_from_matproj", "enrich_all", "factories", + "vis", ] # ============================================================================ diff --git a/src/pymat/core.py b/src/pymat/core.py index 24aaa67..99bcffc 100644 --- a/src/pymat/core.py +++ b/src/pymat/core.py @@ -114,6 +114,9 @@ class _MaterialInternal: # Properties (all domains) properties: AllProperties = field(default_factory=AllProperties) + # Visual representation (mat-vis textures, lazy-fetched) + _vis: Optional[Any] = field(default=None, repr=False) + # Hierarchy (not shown in repr) parent: Optional[_MaterialInternal] = field(default=None, repr=False) _children: Dict[str, _MaterialInternal] = field(default_factory=dict, repr=False) @@ -121,20 +124,18 @@ class _MaterialInternal: def __post_init__(self): """Apply convenience parameters and property groups to properties object.""" - # Apply color if provided + # Apply color → vis.base_color (3.0: vis is the canonical home) if self.color is not None: if len(self.color) == 3: - # RGB provided, add full opacity - self.properties.pbr.base_color = (*self.color, 1.0) + self.vis.base_color = (*self.color, 1.0) elif len(self.color) == 4: - # RGBA provided - self.properties.pbr.base_color = self.color + self.vis.base_color = self.color else: raise ValueError( f"color must be RGB (3 values) or RGBA (4 values), got {len(self.color)}" ) - # Apply property groups + # Apply property groups (non-PBR) if self.mechanical: for key, value in self.mechanical.items(): if hasattr(self.properties.mechanical, key): @@ -155,8 +156,20 @@ def __post_init__(self): if hasattr(self.properties.optical, key): setattr(self.properties.optical, key, value) + # Apply pbr={} kwarg → vis (3.0: vis owns PBR scalars) if self.pbr: + import warnings + + warnings.warn( + "Material(pbr={...}) is deprecated — use vis={...} or set material.vis.* directly. " + "PBR scalars belong on material.vis, not material.properties.pbr.", + DeprecationWarning, + stacklevel=2, + ) for key, value in self.pbr.items(): + if key in self.vis._PBR_SCALAR_FIELDS: + setattr(self.vis, key, value) + # Also sync to properties.pbr for backward compat if hasattr(self.properties.pbr, key): setattr(self.properties.pbr, key, value) @@ -175,22 +188,54 @@ def __post_init__(self): if hasattr(self.properties.sourcing, key): setattr(self.properties.sourcing, key, value) - # Apply PBR defaults from optical properties (DRY principle) - # Allow physics properties to drive rendering defaults, but preserve explicit overrides - - # If ior wasn't explicitly set (still at default 1.5) and optical.refractive_index exists, - # use optical.refractive_index as the PBR ior - if self.properties.pbr.ior == 1.5 and self.properties.optical.refractive_index is not None: - self.properties.pbr.ior = self.properties.optical.refractive_index - - # If transmission wasn't explicitly set (still at default 0.0) and - # optical.transparency exists, convert transparency (0-100%) to - # transmission (0-1) - if ( - self.properties.pbr.transmission == 0.0 - and self.properties.optical.transparency is not None - ): - self.properties.pbr.transmission = self.properties.optical.transparency / 100.0 + # Optical → vis derivations (3.0: target vis, not properties.pbr) + if self.vis.ior is None and self.properties.optical.refractive_index is not None: + self.vis.ior = self.properties.optical.refractive_index + + if self.vis.transmission is None and self.properties.optical.transparency is not None: + self.vis.transmission = self.properties.optical.transparency / 100.0 + + # Sync vis → properties.pbr for backward compat (ocp_vscode reads properties.pbr) + self._sync_vis_to_pbr() + + def _sync_vis_to_pbr(self): + """Sync vis scalars → properties.pbr for backward compat.""" + vis = self._vis + if vis is None: + return + pbr = self.properties.pbr + for field in ("roughness", "metallic", "base_color", "ior", "transmission", "clearcoat", "emissive"): + val = getattr(vis, field, None) + if val is not None and hasattr(pbr, field): + setattr(pbr, field, val) + + # ========================================================================= + # Visual representation (mat-vis) + # ========================================================================= + + @property + def vis(self): + """Visual representation — textures, finishes, source reference. + + Always returns a Vis instance (never None). Starts with + source_id=None for materials without mat-vis data. Populated + from TOML [vis] section for registered materials. + + Usage: + steel.vis.source_id # "ambientcg/Metal_Brushed_001" + steel.vis.textures["color"] # PNG bytes (lazy-fetched) + steel.vis.finishes # {"brushed": "...", ...} + steel.vis.finish = "polished" # switch appearance + """ + if self._vis is None: + from pymat.vis._model import Vis + + self._vis = Vis() + return self._vis + + @vis.setter + def vis(self, value): + self._vis = value # ========================================================================= # Chaining API diff --git a/src/pymat/data/ceramics.toml b/src/pymat/data/ceramics.toml index 94bfcb5..26b0fb4 100644 --- a/src/pymat/data/ceramics.toml +++ b/src/pymat/data/ceramics.toml @@ -30,7 +30,7 @@ dielectric_constant = 9.0 breakdown_voltage_value = 12 breakdown_voltage_unit = "kV/mm" -[alumina.pbr] +[alumina.vis] base_color = [0.95, 0.95, 0.93, 1.0] transmission = 0.0 metallic = 0.0 @@ -83,7 +83,7 @@ thermal_conductivity_unit = "W/(m*K)" [zirconia.electrical] dielectric_constant = 25 -[zirconia.pbr] +[zirconia.vis] base_color = [0.9, 0.88, 0.85, 1.0] transmission = 0.0 metallic = 0.0 @@ -115,7 +115,7 @@ thermal_conductivity_unit = "W/(m*K)" [sic.electrical] dielectric_constant = 10 -[sic.pbr] +[sic.vis] base_color = [0.5, 0.5, 0.5, 1.0] transmission = 0.0 metallic = 0.0 @@ -148,7 +148,7 @@ dielectric_constant = 5.8 volume_resistivity_value = 1e11 volume_resistivity_unit = "ohm*cm" -[macor.pbr] +[macor.vis] base_color = [0.98, 0.98, 0.96, 1.0] transmission = 0.0 metallic = 0.0 @@ -177,7 +177,7 @@ melting_point_unit = "degC" thermal_conductivity_value = 17 thermal_conductivity_unit = "W/(m*K)" -[shapal.pbr] +[shapal.vis] base_color = [0.85, 0.82, 0.8, 1.0] transmission = 0.0 metallic = 0.0 @@ -215,7 +215,7 @@ dielectric_constant = 6.0 breakdown_voltage_value = 15 breakdown_voltage_unit = "kV/mm" -[glass.pbr] +[glass.vis] base_color = [0.9, 0.93, 0.95, 0.85] metallic = 0.0 roughness = 0.1 @@ -259,7 +259,7 @@ grade = "BK7" [glass.BK7.optical] refractive_index = 1.517 -[glass.BK7.pbr] +[glass.BK7.vis] transmission = 0.95 @@ -286,7 +286,7 @@ thermal_conductivity_unit = "W/(m*K)" [beryllia.electrical] dielectric_constant = 7.35 -[beryllia.pbr] +[beryllia.vis] base_color = [0.9, 0.9, 0.9, 1.0] transmission = 0.0 metallic = 0.0 @@ -319,7 +319,7 @@ thermal_conductivity_unit = "W/(m*K)" [yttria.electrical] dielectric_constant = 10 -[yttria.pbr] +[yttria.vis] base_color = [0.95, 0.93, 0.9, 1.0] transmission = 0.0 metallic = 0.0 diff --git a/src/pymat/data/electronics.toml b/src/pymat/data/electronics.toml index ee1d954..0ce87b5 100644 --- a/src/pymat/data/electronics.toml +++ b/src/pymat/data/electronics.toml @@ -32,7 +32,7 @@ volume_resistivity_unit = "ohm*cm" breakdown_voltage_value = 15 breakdown_voltage_unit = "kV/mm" -[fr4.pbr] +[fr4.vis] base_color = [0.15, 0.15, 0.15, 1.0] metallic = 0.0 roughness = 0.7 @@ -60,7 +60,7 @@ density_unit = "g/cm^3" [rogers.electrical] dielectric_constant = 3.5 -[rogers.pbr] +[rogers.vis] base_color = [0.2, 0.2, 0.2, 1.0] metallic = 0.0 roughness = 0.6 @@ -106,7 +106,7 @@ dielectric_constant = 3.5 breakdown_voltage_value = 600 breakdown_voltage_unit = "kV/mm" -[kapton.pbr] +[kapton.vis] base_color = [0.95, 0.85, 0.4, 1.0] metallic = 0.0 roughness = 0.5 @@ -133,7 +133,7 @@ resistivity_unit = "ohm*m" conductivity_value = 5.96e7 conductivity_unit = "S/m" -[copper_pcb.pbr] +[copper_pcb.vis] base_color = [0.72, 0.45, 0.2, 1.0] metallic = 1.0 roughness = 0.35 @@ -154,7 +154,7 @@ grade = "2oz" name = "Gold Plated Copper (ENIG)" treatment = "gold_plated" -[copper_pcb.gold_plated.pbr] +[copper_pcb.gold_plated.vis] base_color = [1.0, 0.84, 0.0, 1.0] metallic = 1.0 roughness = 0.2 @@ -176,7 +176,7 @@ density_unit = "g/cm^3" resistivity_value = 1.5e-7 resistivity_unit = "ohm*m" -[solder.pbr] +[solder.vis] base_color = [0.7, 0.7, 0.7, 1.0] metallic = 1.0 roughness = 0.4 @@ -235,7 +235,7 @@ dielectric_constant = 3.8 volume_resistivity_value = 1e15 volume_resistivity_unit = "ohm*cm" -[epoxy_potting.pbr] +[epoxy_potting.vis] base_color = [0.5, 0.4, 0.3, 1.0] metallic = 0.0 roughness = 0.6 @@ -261,7 +261,7 @@ thermal_conductivity_unit = "W/(m*K)" [silicone_potting.electrical] dielectric_constant = 3.0 -[silicone_potting.pbr] +[silicone_potting.vis] base_color = [0.8, 0.6, 0.4, 1.0] metallic = 0.0 roughness = 0.7 @@ -286,7 +286,7 @@ density_unit = "g/cm^3" permeability = 100 dielectric_constant = 10 -[ferrite.pbr] +[ferrite.vis] base_color = [0.1, 0.1, 0.1, 1.0] metallic = 0.0 roughness = 0.8 @@ -309,7 +309,7 @@ dielectric_constant = 1000 breakdown_voltage_value = 100 breakdown_voltage_unit = "kV/mm" -[ceramic_capacitor.pbr] +[ceramic_capacitor.vis] base_color = [0.95, 0.8, 0.1, 1.0] metallic = 0.0 roughness = 0.5 @@ -342,7 +342,7 @@ thermal_conductivity_unit = "W/(m*K)" thermal_expansion_value = 5.8e-6 thermal_expansion_unit = "1/K" -[alumina_pcb.pbr] +[alumina_pcb.vis] base_color = [0.9, 0.88, 0.85, 1.0] metallic = 0.0 roughness = 0.4 diff --git a/src/pymat/data/gases.toml b/src/pymat/data/gases.toml index 91259d6..5441e83 100644 --- a/src/pymat/data/gases.toml +++ b/src/pymat/data/gases.toml @@ -24,7 +24,7 @@ specific_heat_unit = "J/(kg*K)" [air.optical] refractive_index = 1.000293 -[air.pbr] +[air.vis] base_color = [0.9, 0.95, 1.0, 0.01] metallic = 0.0 roughness = 0.0 @@ -53,7 +53,7 @@ specific_heat_unit = "J/(kg*K)" [nitrogen.optical] refractive_index = 1.000298 -[nitrogen.pbr] +[nitrogen.vis] base_color = [0.9, 0.95, 1.0, 0.01] metallic = 0.0 roughness = 0.0 @@ -75,7 +75,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 2042 specific_heat_unit = "J/(kg*K)" -[nitrogen.liquid.pbr] +[nitrogen.liquid.vis] base_color = [0.85, 0.9, 0.95, 0.4] transmission = 0.9 @@ -101,7 +101,7 @@ specific_heat_unit = "J/(kg*K)" [oxygen.optical] refractive_index = 1.000271 -[oxygen.pbr] +[oxygen.vis] base_color = [0.9, 0.95, 1.0, 0.01] metallic = 0.0 roughness = 0.0 @@ -129,7 +129,7 @@ specific_heat_unit = "J/(kg*K)" [argon.optical] refractive_index = 1.000281 -[argon.pbr] +[argon.vis] base_color = [0.9, 0.95, 1.0, 0.01] metallic = 0.0 roughness = 0.0 @@ -160,7 +160,7 @@ specific_heat_unit = "J/(kg*K)" [co2.optical] refractive_index = 1.000449 -[co2.pbr] +[co2.vis] base_color = [0.9, 0.95, 1.0, 0.01] metallic = 0.0 roughness = 0.0 @@ -180,7 +180,7 @@ density_unit = "g/cm^3" thermal_conductivity_value = 0.6 thermal_conductivity_unit = "W/(m*K)" -[co2.dry_ice.pbr] +[co2.dry_ice.vis] base_color = [0.95, 0.95, 1.0, 0.7] metallic = 0.0 roughness = 0.3 @@ -208,7 +208,7 @@ specific_heat_unit = "J/(kg*K)" [helium.optical] refractive_index = 1.000035 -[helium.pbr] +[helium.vis] base_color = [0.9, 0.95, 1.0, 0.005] metallic = 0.0 roughness = 0.0 @@ -230,7 +230,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 4700 specific_heat_unit = "J/(kg*K)" -[helium.liquid.pbr] +[helium.liquid.vis] base_color = [0.85, 0.9, 0.95, 0.3] transmission = 0.95 @@ -256,7 +256,7 @@ specific_heat_unit = "J/(kg*K)" [hydrogen.optical] refractive_index = 1.000132 -[hydrogen.pbr] +[hydrogen.vis] base_color = [0.9, 0.95, 1.0, 0.005] metallic = 0.0 roughness = 0.0 @@ -287,7 +287,7 @@ specific_heat_unit = "J/(kg*K)" [neon.optical] refractive_index = 1.000067 -[neon.pbr] +[neon.vis] base_color = [1.0, 0.7, 0.3, 0.02] # Slight orange glow hint metallic = 0.0 roughness = 0.0 @@ -315,7 +315,7 @@ specific_heat_unit = "J/(kg*K)" [xenon.optical] refractive_index = 1.000702 -[xenon.pbr] +[xenon.vis] base_color = [0.8, 0.85, 1.0, 0.02] metallic = 0.0 roughness = 0.0 @@ -346,7 +346,7 @@ specific_heat_unit = "J/(kg*K)" [methane.optical] refractive_index = 1.000444 -[methane.pbr] +[methane.vis] base_color = [0.9, 0.95, 1.0, 0.01] metallic = 0.0 roughness = 0.0 @@ -376,7 +376,7 @@ specific_heat_unit = "J/(kg*K)" [vacuum.optical] refractive_index = 1.0 -[vacuum.pbr] +[vacuum.vis] base_color = [0.0, 0.0, 0.0, 0.0] metallic = 0.0 roughness = 0.0 diff --git a/src/pymat/data/liquids.toml b/src/pymat/data/liquids.toml index 65d813a..ce66c0f 100644 --- a/src/pymat/data/liquids.toml +++ b/src/pymat/data/liquids.toml @@ -30,7 +30,7 @@ dielectric_constant = 80.1 resistivity_value = 182000 resistivity_unit = "ohm*m" -[water.pbr] +[water.vis] base_color = [0.7, 0.85, 0.95, 0.3] metallic = 0.0 roughness = 0.0 @@ -52,7 +52,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 2090 specific_heat_unit = "J/(kg*K)" -[water.ice.pbr] +[water.ice.vis] base_color = [0.9, 0.95, 1.0, 0.7] roughness = 0.1 transmission = 0.8 @@ -81,7 +81,7 @@ specific_heat_unit = "J/(kg*K)" [heavy_water.optical] refractive_index = 1.328 -[heavy_water.pbr] +[heavy_water.vis] base_color = [0.7, 0.85, 0.95, 0.3] metallic = 0.0 roughness = 0.0 @@ -114,7 +114,7 @@ dielectric_constant = 2.2 breakdown_voltage_value = 30 breakdown_voltage_unit = "kV/mm" -[mineral_oil.pbr] +[mineral_oil.vis] base_color = [0.95, 0.9, 0.7, 0.4] metallic = 0.0 roughness = 0.0 @@ -145,7 +145,7 @@ specific_heat_unit = "J/(kg*K)" [glycerol.optical] refractive_index = 1.473 -[glycerol.pbr] +[glycerol.vis] base_color = [0.95, 0.95, 0.9, 0.5] metallic = 0.0 roughness = 0.0 @@ -182,7 +182,7 @@ dielectric_constant = 2.7 breakdown_voltage_value = 15 breakdown_voltage_unit = "kV/mm" -[silicone_oil.pbr] +[silicone_oil.vis] base_color = [0.9, 0.9, 0.9, 0.4] metallic = 0.0 roughness = 0.0 diff --git a/src/pymat/data/metals.toml b/src/pymat/data/metals.toml index 6dd1e2a..a00c9dc 100644 --- a/src/pymat/data/metals.toml +++ b/src/pymat/data/metals.toml @@ -23,11 +23,17 @@ specific_heat_unit = "J/(kg*K)" thermal_expansion_value = 16e-6 thermal_expansion_unit = "1/K" -[stainless.pbr] +[stainless.vis] base_color = [0.75, 0.75, 0.77, 1.0] metallic = 1.0 roughness = 0.3 transmission = 0.0 +default = "brushed" + +[stainless.vis.finishes] +brushed = "ambientcg/Metal012" +polished = "ambientcg/Metal049A" +dirty = "ambientcg/Metal049B" # Stainless Steel 304 [stainless.s304] @@ -72,7 +78,7 @@ resistivity_unit = "ohm*m" name = "Stainless Steel 316L - Electropolished" treatment = "electropolished" -[stainless.s316L.electropolished.pbr] +[stainless.s316L.electropolished.vis] roughness = 0.1 transmission = 0.0 @@ -81,7 +87,7 @@ transmission = 0.0 name = "Stainless Steel 316L - Passivated" treatment = "passivated" -[stainless.s316L.passivated.pbr] +[stainless.s316L.passivated.vis] roughness = 0.25 transmission = 0.0 @@ -136,11 +142,16 @@ specific_heat_unit = "J/(kg*K)" thermal_expansion_value = 23.1e-6 thermal_expansion_unit = "1/K" -[aluminum.pbr] +[aluminum.vis] base_color = [0.88, 0.88, 0.88, 1.0] metallic = 1.0 roughness = 0.4 transmission = 0.0 +default = "smooth" + +[aluminum.vis.finishes] +smooth = "ambientcg/Metal049A" +machined = "ambientcg/Metal055A" # Aluminum 6061-T6 [aluminum.a6061] @@ -169,11 +180,37 @@ tensile_strength_unit = "MPa" name = "Aluminum 6061-T6 - Anodized" treatment = "anodized" -[aluminum.a6061.T6.anodized.pbr] +[aluminum.a6061.T6.anodized.vis] base_color = [0.2, 0.2, 0.2, 1.0] roughness = 0.5 transmission = 0.0 +# Aluminum 6063 (extrusion alloy — composition with spec ranges) +[aluminum.a6063] +name = "Aluminum 6063" +grade = "6063" + +# Composition with grade-spec ranges (per AZoM / ASM). +# Nominal values are typical mid-range; ranges are the spec window. +[aluminum.a6063.composition] +Si = {nominal = 0.004, min = 0.002, max = 0.006} +Mg = {nominal = 0.007, min = 0.0045, max = 0.009} +Fe = {nominal = 0.002, max = 0.0035} +Cu = 0.001 +Mn = 0.001 +Zn = 0.001 +Ti = 0.001 +Cr = 0.001 +Al = 0.982 + +[aluminum.a6063.mechanical] +density_value = 2.69 +density_unit = "g/cm^3" +yield_strength_value = 48 +yield_strength_unit = "MPa" +tensile_strength_value = 130 +tensile_strength_unit = "MPa" + # Aluminum 7075-T6 (aerospace) [aluminum.a7075] name = "Aluminum 7075" @@ -243,11 +280,17 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 385 specific_heat_unit = "J/(kg*K)" -[copper.pbr] +[copper.vis] base_color = [0.72, 0.45, 0.2, 1.0] metallic = 1.0 roughness = 0.3 transmission = 0.0 +default = "polished" + +[copper.vis.finishes] +polished = "ambientcg/Metal043A" +oxidized = "ambientcg/Metal043B" +aged = "ambientcg/Metal026" [copper.OFHC] name = "OFHC Copper (Oxygen-Free High Conductivity)" @@ -286,7 +329,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 133 specific_heat_unit = "J/(kg*K)" -[tungsten.pbr] +[tungsten.vis] base_color = [0.2, 0.2, 0.22, 1.0] metallic = 1.0 roughness = 0.4 @@ -333,7 +376,7 @@ melting_point_unit = "degC" thermal_conductivity_value = 35.3 thermal_conductivity_unit = "W/(m*K)" -[lead.pbr] +[lead.vis] base_color = [0.3, 0.3, 0.33, 1.0] metallic = 1.0 roughness = 0.5 @@ -364,11 +407,15 @@ melting_point_unit = "degC" thermal_conductivity_value = 21.9 thermal_conductivity_unit = "W/(m*K)" -[titanium.pbr] +[titanium.vis] base_color = [0.6, 0.6, 0.6, 1.0] metallic = 1.0 roughness = 0.3 transmission = 0.0 +default = "smooth" + +[titanium.vis.finishes] +smooth = "ambientcg/Metal049A" # ============================================================================ @@ -391,11 +438,16 @@ yield_strength_unit = "MPa" melting_point_value = 900 melting_point_unit = "degC" -[brass.pbr] +[brass.vis] base_color = [0.88, 0.78, 0.5, 1.0] metallic = 1.0 roughness = 0.25 transmission = 0.0 +default = "polished" + +[brass.vis.finishes] +polished = "ambientcg/Metal008" +oxidized = "ambientcg/Metal035" # ============================================================================ @@ -420,7 +472,7 @@ tensile_strength_unit = "MPa" melting_point_value = 1480 melting_point_unit = "degC" -[havar.pbr] +[havar.vis] base_color = [0.7, 0.7, 0.72, 1.0] metallic = 1.0 roughness = 0.3 @@ -453,7 +505,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 265 specific_heat_unit = "J/(kg*K)" -[niobium.pbr] +[niobium.vis] base_color = [0.65, 0.65, 0.68, 1.0] metallic = 1.0 roughness = 0.3 @@ -482,7 +534,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 235 specific_heat_unit = "J/(kg*K)" -[silver.pbr] +[silver.vis] base_color = [0.95, 0.95, 0.95, 1.0] metallic = 1.0 roughness = 0.15 @@ -511,7 +563,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 129 specific_heat_unit = "J/(kg*K)" -[gold.pbr] +[gold.vis] base_color = [1.0, 0.84, 0.0, 1.0] metallic = 1.0 roughness = 0.15 @@ -544,7 +596,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 251 specific_heat_unit = "J/(kg*K)" -[molybdenum.pbr] +[molybdenum.vis] base_color = [0.55, 0.55, 0.58, 1.0] metallic = 1.0 roughness = 0.3 @@ -571,7 +623,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 371 specific_heat_unit = "J/(kg*K)" -[gallium.pbr] +[gallium.vis] base_color = [0.76, 0.79, 0.85, 1.0] metallic = 1.0 roughness = 0.2 @@ -600,7 +652,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 122 specific_heat_unit = "J/(kg*K)" -[bismuth.pbr] +[bismuth.vis] base_color = [0.75, 0.70, 0.72, 1.0] metallic = 1.0 roughness = 0.4 @@ -629,7 +681,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 243 specific_heat_unit = "J/(kg*K)" -[rhodium.pbr] +[rhodium.vis] base_color = [0.85, 0.85, 0.87, 1.0] metallic = 1.0 roughness = 0.2 @@ -658,7 +710,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 298 specific_heat_unit = "J/(kg*K)" -[yttrium.pbr] +[yttrium.vis] base_color = [0.78, 0.78, 0.78, 1.0] metallic = 1.0 roughness = 0.3 @@ -681,7 +733,7 @@ density_unit = "g/cm^3" melting_point_value = 700 melting_point_unit = "degC" -[radium.pbr] +[radium.vis] base_color = [0.9, 0.9, 0.85, 1.0] metallic = 1.0 roughness = 0.4 @@ -733,7 +785,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 444 specific_heat_unit = "J/(kg*K)" -[nickel.pbr] +[nickel.vis] base_color = [0.75, 0.75, 0.73, 1.0] metallic = 1.0 roughness = 0.3 @@ -762,7 +814,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 449 specific_heat_unit = "J/(kg*K)" -[iron.pbr] +[iron.vis] base_color = [0.56, 0.57, 0.58, 1.0] metallic = 1.0 roughness = 0.4 @@ -791,7 +843,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 388 specific_heat_unit = "J/(kg*K)" -[zinc.pbr] +[zinc.vis] base_color = [0.72, 0.73, 0.76, 1.0] metallic = 1.0 roughness = 0.35 @@ -820,7 +872,7 @@ thermal_conductivity_unit = "W/(m*K)" specific_heat_value = 228 specific_heat_unit = "J/(kg*K)" -[tin.pbr] +[tin.vis] base_color = [0.85, 0.85, 0.83, 1.0] metallic = 1.0 roughness = 0.3 diff --git a/src/pymat/data/plastics.toml b/src/pymat/data/plastics.toml index 6c672cb..b7b88f0 100644 --- a/src/pymat/data/plastics.toml +++ b/src/pymat/data/plastics.toml @@ -34,7 +34,7 @@ dielectric_constant = 3.2 volume_resistivity_value = 1e16 volume_resistivity_unit = "ohm*cm" -[peek.pbr] +[peek.vis] base_color = [0.82, 0.7, 0.55, 1.0] transmission = 0.0 metallic = 0.0 @@ -126,7 +126,7 @@ dielectric_constant = 3.7 volume_resistivity_value = 1e17 volume_resistivity_unit = "ohm*cm" -[delrin.pbr] +[delrin.vis] base_color = [0.9, 0.9, 0.85, 1.0] transmission = 0.0 metallic = 0.0 @@ -168,7 +168,7 @@ thermal_conductivity_unit = "W/(m*K)" [ultem.electrical] dielectric_constant = 3.15 -[ultem.pbr] +[ultem.vis] base_color = [0.85, 0.65, 0.2, 1.0] transmission = 0.0 metallic = 0.0 @@ -209,7 +209,7 @@ dielectric_constant = 2.1 volume_resistivity_value = 1e18 volume_resistivity_unit = "ohm*cm" -[ptfe.pbr] +[ptfe.vis] base_color = [0.95, 0.95, 0.95, 1.0] transmission = 0.0 metallic = 0.0 @@ -227,7 +227,7 @@ treatment = "reflector" transparency = 0 reflectivity = 98 -[ptfe.reflector.pbr] +[ptfe.reflector.vis] base_color = [0.98, 0.98, 0.98, 1.0] transmission = 0.0 roughness = 0.2 @@ -255,7 +255,7 @@ max_service_temp_unit = "degC" transparency = 0 reflectivity = 98.5 -[esr.pbr] +[esr.vis] base_color = [0.99, 0.99, 0.99, 1.0] transmission = 0.0 metallic = 0.0 @@ -287,7 +287,7 @@ glass_transition_unit = "degC" melting_point_value = 215 melting_point_unit = "degC" -[nylon.pbr] +[nylon.vis] base_color = [0.85, 0.85, 0.8, 1.0] transmission = 0.0 metallic = 0.0 @@ -323,7 +323,7 @@ glass_transition_unit = "degC" melting_point_value = 160 melting_point_unit = "degC" -[pla.pbr] +[pla.vis] base_color = [0.8, 0.8, 0.8, 1.0] transmission = 0.0 metallic = 0.0 @@ -355,7 +355,7 @@ glass_transition_unit = "degC" melting_point_value = 225 melting_point_unit = "degC" -[abs.pbr] +[abs.vis] base_color = [0.3, 0.3, 0.3, 1.0] transmission = 0.0 metallic = 0.0 @@ -384,7 +384,7 @@ glass_transition_unit = "degC" melting_point_value = 225 melting_point_unit = "degC" -[petg.pbr] +[petg.vis] base_color = [0.85, 0.85, 0.8, 1.0] transmission = 0.0 metallic = 0.0 @@ -405,7 +405,7 @@ name = "TPU (Thermoplastic Polyurethane)" density_value = 1.21 density_unit = "g/cm^3" -[tpu.pbr] +[tpu.vis] base_color = [0.6, 0.6, 0.6, 0.8] transmission = 0.0 metallic = 0.0 @@ -436,7 +436,7 @@ yield_strength_unit = "MPa" melting_point_value = 400 melting_point_unit = "degC" -[vespel.pbr] +[vespel.vis] base_color = [0.4, 0.2, 0.1, 1.0] transmission = 0.0 metallic = 0.0 @@ -458,7 +458,7 @@ yield_strength_unit = "MPa" melting_point_value = 330 melting_point_unit = "degC" -[torlon.pbr] +[torlon.vis] base_color = [0.85, 0.6, 0.3, 1.0] transmission = 0.0 metallic = 0.0 @@ -478,7 +478,7 @@ yield_strength_unit = "MPa" melting_point_value = 217 melting_point_unit = "degC" -[pctfe.pbr] +[pctfe.vis] base_color = [0.9, 0.9, 0.95, 1.0] transmission = 0.0 metallic = 0.0 @@ -522,7 +522,7 @@ dielectric_constant = 3.0 volume_resistivity_value = 1e14 volume_resistivity_unit = "ohm*cm" -[pmma.pbr] +[pmma.vis] base_color = [0.95, 0.95, 0.95, 0.9] metallic = 0.0 roughness = 0.1 @@ -568,7 +568,7 @@ dielectric_constant = 2.3 volume_resistivity_value = 1e16 volume_resistivity_unit = "ohm*cm" -[pe.pbr] +[pe.vis] base_color = [0.9, 0.9, 0.88, 1.0] transmission = 0.0 metallic = 0.0 @@ -679,7 +679,7 @@ dielectric_constant = 2.9 volume_resistivity_value = 1e14 volume_resistivity_unit = "ohm*cm" -[pc.pbr] +[pc.vis] base_color = [0.92, 0.92, 0.92, 0.85] metallic = 0.0 roughness = 0.15 diff --git a/src/pymat/data/scintillators.toml b/src/pymat/data/scintillators.toml index 676ecce..476bab3 100644 --- a/src/pymat/data/scintillators.toml +++ b/src/pymat/data/scintillators.toml @@ -17,7 +17,7 @@ emission_peak = 420 radiation_length = 1.14 interaction_length = 25 -[lyso.pbr] +[lyso.vis] base_color = [0.0, 1.0, 1.0, 0.85] metallic = 0.0 roughness = 0.3 @@ -82,7 +82,7 @@ emission_peak = 480 radiation_length = 1.12 interaction_length = 11 -[bgo.pbr] +[bgo.vis] base_color = [0.58, 0.44, 0.86, 0.8] metallic = 0.0 roughness = 0.3 @@ -114,7 +114,7 @@ emission_peak = 410 radiation_length = 2.59 interaction_length = 27 -[nai.pbr] +[nai.vis] base_color = [1.0, 0.9, 0.7, 0.7] metallic = 0.0 roughness = 0.2 @@ -159,7 +159,7 @@ emission_peak = 420 radiation_length = 1.86 interaction_length = 15 -[csi.pbr] +[csi.vis] base_color = [0.9, 0.9, 0.7, 0.75] metallic = 0.0 roughness = 0.25 @@ -209,7 +209,7 @@ emission_peak = 380 radiation_length = 2.0 interaction_length = 20 -[labr3.pbr] +[labr3.vis] base_color = [0.8, 0.7, 0.9, 0.8] metallic = 0.0 roughness = 0.3 @@ -237,7 +237,7 @@ emission_peak = 420 radiation_length = 0.89 interaction_length = 10.7 -[pwo.pbr] +[pwo.vis] base_color = [0.4, 0.4, 0.45, 0.85] metallic = 0.0 roughness = 0.35 @@ -261,7 +261,7 @@ light_yield = 12000 decay_time = 2.3 emission_peak = 425 -[plastic_scint.pbr] +[plastic_scint.vis] base_color = [0.9, 0.9, 0.85, 0.9] metallic = 0.0 roughness = 0.4 diff --git a/src/pymat/loader.py b/src/pymat/loader.py index 0a93b7b..7cd64b3 100644 --- a/src/pymat/loader.py +++ b/src/pymat/loader.py @@ -19,6 +19,8 @@ else: import tomli as tomllib +from uncertainties import ufloat + if TYPE_CHECKING: from .core import Material @@ -32,6 +34,57 @@ logger = logging.getLogger(__name__) +def _parse_value(val: Any) -> float: + """Parse a scalar value that may be a plain float or a range/uncertainty dict. + + Accepted forms: + 0.4 → ufloat(0.4, 0) + {nominal = 0.4, stddev = 0.1} → ufloat(0.4, 0.1) + {min = 0.2, max = 0.6} → ufloat(0.4, 0.2) (midpoint ± half-range) + {nominal = 0.4, min = 0.2, max = 0.6} → ufloat(0.4, 0.2) + + Returns a ufloat when uncertainty info is present, plain float otherwise. + """ + if isinstance(val, (int, float)): + return float(val) + + if isinstance(val, dict): + nominal = val.get("nominal") + stddev = val.get("stddev") + lo = val.get("min") + hi = val.get("max") + + if nominal is None and lo is not None and hi is not None: + nominal = (lo + hi) / 2.0 + + if nominal is None: + raise ValueError(f"Cannot parse value: {val}") + + if stddev is not None: + return ufloat(nominal, stddev) + elif lo is not None and hi is not None: + return ufloat(nominal, (hi - lo) / 2.0) + else: + return float(nominal) + + return float(val) + + +def _parse_composition(comp: Any) -> dict[str, Any] | None: + """Parse a composition dict, handling range/uncertainty values. + + TOML examples: + composition = {Fe = 0.68, Cr = 0.18} # plain + composition = {Si = {min = 0.2, max = 0.6}} # range + composition = {Fe = {nominal = 0.68, stddev = 0.02}} # uncertainty + """ + if comp is None: + return None + if not isinstance(comp, dict): + return comp + return {el: _parse_value(val) for el, val in comp.items()} + + def _build_properties_from_dict( data: Dict[str, Any], parent_props: Optional[AllProperties] = None ) -> AllProperties: @@ -123,12 +176,21 @@ def update_properties(prop_obj, prop_dict, prop_name): if "optical" in data: update_properties(props.optical, data["optical"], "optical") if "pbr" in data: + import warnings + + warnings.warn( + "TOML [pbr] section is deprecated — use [vis] instead. " + "PBR scalars belong under [material.vis]. " + "See migration guide: https://github.com/MorePET/mat/issues/40", + DeprecationWarning, + stacklevel=4, + ) pbr_data = data["pbr"] - # Special handling for tuples if "base_color" in pbr_data: pbr_data["base_color"] = tuple(pbr_data["base_color"]) if "emissive" in pbr_data: pbr_data["emissive"] = tuple(pbr_data["emissive"]) + # Route to properties.pbr for backward compat update_properties(props.pbr, pbr_data, "pbr") if "manufacturing" in data: update_properties(props.manufacturing, data["manufacturing"], "manufacturing") @@ -165,7 +227,7 @@ def _resolve_material_node( # Extract material-level attributes name = data.get("name", key) formula = data.get("formula") - composition = data.get("composition") + composition = _parse_composition(data.get("composition")) grade = data.get("grade") temper = data.get("temper") treatment = data.get("treatment") @@ -206,6 +268,14 @@ def _resolve_material_node( _key=key, ) + # Populate vis from [vis] section if present + vis_data = data.get("vis") + if vis_data and isinstance(vis_data, dict): + from pymat.vis._model import Vis + + material._vis = Vis.from_toml(vis_data) + material._sync_vis_to_pbr() # backward compat for ocp_vscode + # Register for direct access registry.register(key, material) @@ -230,6 +300,7 @@ def _resolve_material_node( "temper", "treatment", "vendor", + "vis", ): child_material = _resolve_material_node( child_key, diff --git a/src/pymat/vis/__init__.py b/src/pymat/vis/__init__.py new file mode 100644 index 0000000..7183000 --- /dev/null +++ b/src/pymat/vis/__init__.py @@ -0,0 +1,139 @@ +""" +Visual material data from mat-vis. + +Public API — all functions importable from `pymat.vis` directly: + + from pymat import vis + + vis.search(category="metal", roughness=0.3) + vis.fetch("ambientcg", "Metal032", tier="1k") + vis.prefetch("ambientcg", tier="1k") + vis.get_manifest() + vis.rowmap_entry("ambientcg", "Metal032", tier="1k") + +Powered by mat-vis-client (installed separately or from git). +Material.vis wires into this module for lazy texture loading. +""" + +import mat_vis_client +from mat_vis_client import ( + MatVisClient, + fetch, + get_manifest, + prefetch, + rowmap_entry, + seed_indexes, + search as _upstream_search, +) + +# Re-export the full adapters module so new adapters (e.g. to_ktx2) +# are available as soon as mat-vis-client ships them +from mat_vis_client import adapters # noqa: F401 + +from typing import Any + + +def search( + *, + category: str | None = None, + tags: list[str] | None = None, + roughness: float | None = None, + metalness: float | None = None, + source: str | None = None, + limit: int = 20, +) -> list[dict[str, Any]]: + """Search the mat-vis index by category, tags, and scalar similarity. + + Args: + category: filter by canonical category (metal, wood, stone, ...) + tags: require ALL these tags to be present in the entry's tags list + roughness / metalness: score by scalar distance (if set in index) + source: limit to one source + limit: max results + + Does NOT filter by tier — search is for finding materials, + tier is a fetch-time concern. + """ + from mat_vis_client import _get_client + + client = _get_client() + + roughness_range = None + if roughness is not None: + roughness_range = (max(0.0, roughness - 0.2), min(1.0, roughness + 0.2)) + + metalness_range = None + if metalness is not None: + metalness_range = (max(0.0, metalness - 0.2), min(1.0, metalness + 0.2)) + + required_tags = set(t.lower() for t in (tags or [])) + + # Search all sources, no tier filter + sources = [source] if source else client.sources() + results: list[dict] = [] + for src in sources: + try: + for entry in client.index(src): + if category and entry.get("category") != category: + continue + entry_tags = set(t.lower() for t in entry.get("tags", [])) + if required_tags and not required_tags.issubset(entry_tags): + continue + if roughness_range and not ( + entry.get("roughness") is not None + and roughness_range[0] <= entry["roughness"] <= roughness_range[1] + ): + continue + if metalness_range and not ( + entry.get("metalness") is not None + and metalness_range[0] <= entry["metalness"] <= metalness_range[1] + ): + continue + results.append(entry) + except Exception: + continue + + # Score by scalar distance + for r in results: + score = 0.0 + if roughness is not None and r.get("roughness") is not None: + score += abs(r["roughness"] - roughness) + if metalness is not None and r.get("metalness") is not None: + score += abs(r["metalness"] - metalness) + r["score"] = score + + results.sort(key=lambda r: r["score"]) + return results[:limit] + + +def client() -> MatVisClient: + """Get the shared MatVisClient instance (lazy-initialized). + + Future-proof access point — any new methods mat-vis-client adds + are available immediately without pymat code changes: + + c = vis.client() + c.tiers() # discover available tiers + c.sources("1k") # discover sources for a tier + c.search("metal") # search by category + c.fetch_all_textures("ambientcg", "Metal032", tier="1k") + """ + from mat_vis_client import _get_client + + return _get_client() + + +__all__ = [ + # Factory — future-proof, exposes full mat-vis-client API + "client", + # Convenience functions (delegates to client singleton) + "search", + "fetch", + "prefetch", + "rowmap_entry", + "get_manifest", + "seed_indexes", + "MatVisClient", + # Adapters module — new adapters auto-available + "adapters", +] diff --git a/src/pymat/vis/_model.py b/src/pymat/vis/_model.py new file mode 100644 index 0000000..ff1157e --- /dev/null +++ b/src/pymat/vis/_model.py @@ -0,0 +1,257 @@ +""" +Vis model — the visual representation attached to a Material. + +Material.vis returns a Vis instance. It holds: +- source_id: pointer to a mat-vis appearance +- finishes: dict of finish_name → source_id (for TOML-registered materials) +- textures: dict of channel → PNG bytes (lazy-fetched, cached) +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, ClassVar + + +@dataclass +class ResolvedChannel: + """Result of resolving a channel across texture + scalar sources.""" + + texture: bytes | None = None # PNG bytes if texture map available + scalar: float | None = None # scalar fallback from properties.pbr + has_texture: bool = False + + +@dataclass +class Vis: + """Visual representation of a material, backed by mat-vis data. + + Always instantiated (never None on Material). Starts with + source_id=None and empty textures for custom materials. + Populated from TOML [vis] section for registered materials. + + Usage: + steel.vis.source_id # "ambientcg/Metal_Brushed_001" + steel.vis.textures["color"] # PNG bytes (lazy-fetched) + steel.vis.finishes # {"brushed": "...", "polished": "..."} + steel.vis.finish = "polished" # switch appearance + """ + + source_id: str | None = None + tier: str = "1k" + finishes: dict[str, str] = field(default_factory=dict) + + # PBR scalars — can be set from [vis] section in TOML. + # When set here, these take precedence over properties.pbr values. + # In 3.0, properties.pbr will be removed and these become the + # single source for rendering scalars. + roughness: float | None = None + metallic: float | None = None + base_color: tuple | None = None + ior: float | None = None + transmission: float | None = None + clearcoat: float | None = None + emissive: tuple | None = None + + _finish: str | None = None + _textures: dict[str, bytes] = field(default_factory=dict, repr=False) + _fetched: bool = False + + @property + def finish(self) -> str | None: + """Current finish name, or None if using source_id directly.""" + return self._finish + + @finish.setter + def finish(self, name: str) -> None: + """Switch to a named finish. Clears cached textures.""" + if name not in self.finishes: + available = list(self.finishes.keys()) + raise ValueError( + f"Unknown finish '{name}'. Available: {available}" + ) + self._finish = name + self.source_id = self.finishes[name] + self._textures.clear() + self._fetched = False + + @property + def textures(self) -> dict[str, bytes]: + """Channel → PNG bytes. Lazy-fetched on first access. + + Returns empty dict if source_id is None (no appearance set). + """ + if self.source_id is None: + return {} + + if not self._fetched: + self._fetch() + + return self._textures + + def resolve(self, channel: str, scalar: float | None = None) -> ResolvedChannel: + """Resolve a channel: texture if available, scalar fallback. + + Args: + channel: Channel name ("roughness", "metalness", etc.) + scalar: Scalar fallback value (from properties.pbr). + + Returns: + ResolvedChannel with texture bytes and/or scalar value. + """ + tex = self.textures.get(channel) + return ResolvedChannel( + texture=tex, + scalar=scalar, + has_texture=tex is not None, + ) + + def discover( + self, + *, + category: str | None = None, + roughness: float | None = None, + metallic: float | None = None, + limit: int = 5, + auto_set: bool = False, + ) -> list[dict[str, Any]]: + """Search mat-vis for appearances matching this material's scalars. + + Does NOT set source_id automatically — returns candidates for + the user to review. Pass auto_set=True to pick the top match. + + Args: + category: Filter by category. If None, tries to infer from + the material's existing PBR properties. + roughness: Target roughness. If None, reads from the material. + metallic: Target metalness. If None, reads from the material. + limit: Max candidates to return. + auto_set: If True, set source_id to the top match. + + Returns: + List of candidate dicts with "id", "source", "category", + "score". Sorted by score (lower = closer match). + + Example: + candidates = steel.vis.discover(category="metal") + # [{"id": "ambientcg/Metal032", "score": 0.05}, ...] + steel.vis.source_id = candidates[0]["id"] # manual pick + # or: + steel.vis.discover(category="metal", auto_set=True) + """ + from mat_vis_client import search + + results = search( + category=category, + roughness=roughness, + metalness=metallic, + limit=limit, + ) + + # Reformat ids as "source/id" for direct assignment + for r in results: + if "source" in r and "id" in r and "/" not in r["id"]: + r["id"] = f"{r['source']}/{r['id']}" + + if auto_set and results: + self.source_id = results[0]["id"] + self._textures.clear() + self._fetched = False + + return results + + def _fetch(self) -> None: + """Fetch textures via the vis client. Called lazily.""" + if self.source_id is None: + return + + # Parse "source/material_id" format + parts = self.source_id.split("/", 1) + if len(parts) != 2: + raise ValueError( + f"Invalid source_id '{self.source_id}'. " + f"Expected 'source/material_id' format." + ) + source, material_id = parts + + from mat_vis_client import fetch + + self._textures = fetch(source, material_id, tier=self.tier) + self._fetched = True + + _PBR_SCALAR_FIELDS = ("roughness", "metallic", "base_color", "ior", "transmission", "clearcoat", "emissive") + + _PBR_DEFAULTS: ClassVar[dict[str, Any]] = { + "roughness": 0.5, + "metallic": 0.0, + "base_color": (0.8, 0.8, 0.8, 1.0), + "ior": 1.5, + "transmission": 0.0, + "clearcoat": 0.0, + "emissive": (0, 0, 0), + } + + def get(self, field: str, default: Any = None) -> Any: + """Get a PBR scalar with fallback to default. + + Returns the field value if set (not None), otherwise the + default. If no default provided, uses _PBR_DEFAULTS. + + Usage: + vis.get("roughness") # → 0.3 if set, 0.5 if None + vis.get("roughness", 0.0) # → 0.3 if set, 0.0 if None + """ + val = getattr(self, field, None) + if val is not None: + return val + if default is not None: + return default + return self._PBR_DEFAULTS.get(field) + + @classmethod + def from_toml(cls, vis_data: dict[str, Any]) -> Vis: + """Construct from a TOML [vis] section. + + Accepts PBR scalars alongside finishes/source_id. When PBR + scalars are present in [vis], they become the canonical source + and are synced back to properties.pbr for backward compat. + + TOML structure: + [material.vis] + default = "brushed" + roughness = 0.3 + metallic = 1.0 + base_color = [0.75, 0.75, 0.77, 1.0] + + [material.vis.finishes] + brushed = "ambientcg/Metal_Brushed_001" + polished = "ambientcg/Metal_Polished_002" + """ + finishes = vis_data.get("finishes", {}) + default_finish = vis_data.get("default") + + source_id = None + finish = None + if default_finish and default_finish in finishes: + source_id = finishes[default_finish] + finish = default_finish + elif finishes: + finish = next(iter(finishes)) + source_id = finishes[finish] + + # Extract PBR scalars from [vis] section + scalars = {} + for field in cls._PBR_SCALAR_FIELDS: + if field in vis_data: + val = vis_data[field] + if isinstance(val, list): + val = tuple(val) + scalars[field] = val + + return cls( + source_id=source_id, + finishes=finishes, + _finish=finish, + **scalars, + ) diff --git a/src/pymat/vis/adapters.py b/src/pymat/vis/adapters.py new file mode 100644 index 0000000..7427a89 --- /dev/null +++ b/src/pymat/vis/adapters.py @@ -0,0 +1,81 @@ +""" +Output adapters — thin wrappers that map Material to mat-vis's +generic adapter functions. + +The actual format logic (Three.js field names, glTF schema, +MaterialX XML) lives in mat_vis_client.adapters (installed from +mat-vis-client package). These wrappers extract scalars + textures +from a Material and pass them through. + + from pymat.vis.adapters import to_threejs, to_gltf, export_mtlx + result = to_threejs(material) +""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from mat_vis_client.adapters import export_mtlx as _export_mtlx +from mat_vis_client.adapters import to_gltf as _to_gltf +from mat_vis_client.adapters import to_threejs as _to_threejs + +if TYPE_CHECKING: + from pymat.core import _MaterialInternal as Material + + +def _extract_scalars(material: Material) -> dict[str, Any]: + """Extract PBR scalars from material.vis with defaults. + + Maps py-mat "metallic" → mat-vis "metalness". + """ + vis = material.vis + return { + "metalness": vis.get("metallic"), + "roughness": vis.get("roughness"), + "base_color": vis.get("base_color"), + "ior": vis.get("ior"), + "transmission": vis.get("transmission"), + "clearcoat": vis.get("clearcoat"), + "emissive": vis.get("emissive"), + } + + +def _extract_textures(material: Material) -> dict[str, bytes]: + """Extract texture bytes from Material.vis.""" + if material.vis.source_id is None: + return {} + return material.vis.textures + + +def to_threejs(material: Material) -> dict[str, Any]: + """Format as a Three.js MeshPhysicalMaterial-compatible dict. + + Reads PBR scalars and texture maps from material.vis. + Delegates to mat-vis's generic adapter. + """ + return _to_threejs(_extract_scalars(material), _extract_textures(material)) + + +def to_gltf(material: Material) -> dict[str, Any]: + """Format as a glTF pbrMetallicRoughness material dict. + + Delegates to mat-vis's generic adapter. + """ + result = _to_gltf(_extract_scalars(material), _extract_textures(material)) + result["name"] = material.name + return result + + +def export_mtlx(material: Material, output_dir: Path) -> Path: + """Export as a MaterialX .mtlx file + PNG textures on disk. + + Delegates to mat-vis's generic adapter. + """ + safe_name = material.name.replace(" ", "_").replace("/", "_") + return _export_mtlx( + _extract_scalars(material), + _extract_textures(material), + output_dir, + material_name=safe_name, + ) diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..a87d840 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,123 @@ +"""Tests for pymat.vis.adapters — Material → format wrappers.""" + +from __future__ import annotations + +import tempfile +from pathlib import Path + +from pymat import Material +from pymat.vis.adapters import ( + _extract_scalars, + _extract_textures, + export_mtlx, + to_gltf, + to_threejs, +) + + +def _make_material(with_vis=False): + """Create a test material with vis scalars.""" + m = Material(name="Test Steel") + m.vis.metallic = 1.0 + m.vis.roughness = 0.3 + m.vis.base_color = (0.8, 0.8, 0.8, 1.0) + m.vis.ior = 2.5 + m.vis.transmission = 0.0 + m.vis.clearcoat = 0.0 + m.vis.emissive = (0, 0, 0) + if with_vis: + m.vis._textures = { + "color": b"\x89PNG_color", + "normal": b"\x89PNG_normal", + "roughness": b"\x89PNG_roughness", + } + m.vis._fetched = True + m.vis.source_id = "ambientcg/Metal032" + return m + + +class TestExtractScalars: + def test_from_pbr(self): + m = _make_material() + s = _extract_scalars(m) + assert s["metalness"] == 1.0 + assert s["roughness"] == 0.3 + assert s["ior"] == 2.5 + + def test_vis_wins_over_pbr(self): + m = _make_material() + m.vis.roughness = 0.5 # vis override + s = _extract_scalars(m) + assert s["roughness"] == 0.5 # vis wins + assert s["metalness"] == 1.0 # falls back to pbr + + def test_metallic_mapped_to_metalness(self): + m = _make_material() + s = _extract_scalars(m) + assert "metalness" in s + assert "metallic" not in s + + +class TestExtractTextures: + def test_no_vis_returns_empty(self): + m = _make_material(with_vis=False) + assert _extract_textures(m) == {} + + def test_with_vis_returns_textures(self): + m = _make_material(with_vis=True) + tex = _extract_textures(m) + assert "color" in tex + assert tex["color"] == b"\x89PNG_color" + + +class TestToThreejs: + def test_scalar_only(self): + m = _make_material(with_vis=False) + d = to_threejs(m) + assert d["metalness"] == 1.0 + assert d["roughness"] == 0.3 + assert "map" not in d # no textures + + def test_with_textures(self): + m = _make_material(with_vis=True) + d = to_threejs(m) + assert "map" in d # base64 data URI + assert d["map"].startswith("data:image/png;base64,") + assert "normalMap" in d + assert "roughnessMap" in d + + +class TestToGltf: + def test_scalar_only(self): + m = _make_material(with_vis=False) + d = to_gltf(m) + assert d["name"] == "Test Steel" + assert "pbrMetallicRoughness" in d + pbr = d["pbrMetallicRoughness"] + assert pbr["metallicFactor"] == 1.0 + assert pbr["roughnessFactor"] == 0.3 + + def test_with_textures(self): + m = _make_material(with_vis=True) + d = to_gltf(m) + assert d["name"] == "Test Steel" + + +class TestExportMtlx: + def test_export_creates_files(self): + m = _make_material(with_vis=True) + with tempfile.TemporaryDirectory() as tmp: + mtlx_path = export_mtlx(m, Path(tmp)) + assert mtlx_path.exists() + assert mtlx_path.suffix == ".mtlx" + # Check PNGs were written + pngs = list(Path(tmp).glob("*.png")) + assert len(pngs) == 3 # color, normal, roughness + + def test_export_no_textures(self): + m = _make_material(with_vis=False) + with tempfile.TemporaryDirectory() as tmp: + mtlx_path = export_mtlx(m, Path(tmp)) + assert mtlx_path.exists() + pngs = list(Path(tmp).glob("*.png")) + assert len(pngs) == 0 # no textures diff --git a/tests/test_catalog.py b/tests/test_catalog.py new file mode 100644 index 0000000..09aa8ca --- /dev/null +++ b/tests/test_catalog.py @@ -0,0 +1,82 @@ +"""Tests for the catalog generator script.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + + +class TestCatalogGenerator: + def test_generates_root_index(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + readme = tmp_path / "README.md" + assert readme.exists() + content = readme.read_text() + assert "Material Catalog" in content + assert "Metals" in content + + def test_generates_category_dirs(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + assert (tmp_path / "metals" / "README.md").exists() + assert (tmp_path / "plastics" / "README.md").exists() + assert (tmp_path / "scintillators" / "README.md").exists() + + def test_generates_material_pages(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + # stainless should have a page + assert (tmp_path / "metals" / "stainless.md").exists() + content = (tmp_path / "metals" / "stainless.md").read_text() + assert "Stainless Steel" in content + assert "Density" in content or "Roughness" in content + + def test_material_page_has_vis_section(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + content = (tmp_path / "metals" / "stainless.md").read_text() + # stainless has [vis] with source_id + assert "mat-vis" in content or "ambientcg" in content + + def test_material_page_has_composition(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + content = (tmp_path / "metals" / "stainless.md").read_text() + assert "Composition" in content + assert "Fe" in content + assert "Cr" in content + + def test_uncertainty_composition_renders(self, tmp_path): + """Al 6063 has ufloat composition — should render without crash.""" + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + page = tmp_path / "metals" / "aluminum-a6063.md" + assert page.exists() + content = page.read_text() + assert "Si" in content + # Should show uncertainty notation + assert "±" in content + + def test_category_index_has_table(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + content = (tmp_path / "metals" / "README.md").read_text() + assert "| Material |" in content + assert "Stainless Steel" in content + + def test_total_material_count(self, tmp_path): + from scripts.generate_catalog import generate + + generate(tmp_path, skip_thumbnails=True) + md_files = list(tmp_path.rglob("*.md")) + # Should have at least 90+ pages (96 materials + 7 category indexes + 1 root) + assert len(md_files) > 90 diff --git a/tests/test_e2e_vis.py b/tests/test_e2e_vis.py new file mode 100644 index 0000000..662c445 --- /dev/null +++ b/tests/test_e2e_vis.py @@ -0,0 +1,155 @@ +"""End-to-end test: Material → vis → mat-vis live data → adapter output. + +Hits real mat-vis release assets. Skip with MAT_VIS_SKIP_LIVE=1. +""" + +from __future__ import annotations + +import os + +import pytest + +SKIP_LIVE = os.environ.get("MAT_VIS_SKIP_LIVE", "0") == "1" + + +@pytest.mark.skipif(SKIP_LIVE, reason="MAT_VIS_SKIP_LIVE=1") +class TestEndToEnd: + """Full pipeline: TOML material → vis textures → Three.js output.""" + + def test_search_and_fetch(self): + """Search the mat-vis index, fetch textures for a result.""" + from pymat import vis + + # Search for metals in the corpus + results = vis.search(category="metal", limit=3) + assert len(results) > 0, "No metals found in mat-vis index" + + mat_id = results[0]["id"] + source = results[0].get("source", "ambientcg") + assert mat_id, "Empty material ID" + + # Fetch textures via HTTP range read + textures = vis.fetch(source, mat_id, tier="1k") + assert len(textures) > 0, f"No textures for {source}/{mat_id}" + + # Verify PNG bytes + for channel, data in textures.items(): + assert data[:4] == b"\x89PNG", f"{channel}: not a valid PNG" + assert len(data) > 100, f"{channel}: suspiciously small ({len(data)} bytes)" + + def test_material_vis_textures(self): + """Material.vis.textures fetches real PBR textures.""" + from pymat import Material, vis + + # Find a material that exists in mat-vis + results = vis.search(category="wood", limit=1) + if not results: + pytest.skip("No wood materials in mat-vis index") + + source = results[0].get("source", "ambientcg") + mat_id = results[0]["id"] + + # Create a material and wire vis + m = Material(name="Test Wood") + m.vis.roughness = 0.6 + m.vis.source_id = f"{source}/{mat_id}" + + # Access textures — triggers lazy HTTP fetch + textures = m.vis.textures + assert len(textures) > 0, f"No textures fetched for {source}/{mat_id}" + assert all(v[:4] == b"\x89PNG" for v in textures.values()) + + def test_material_to_threejs_with_live_textures(self): + """Full path: Material → vis.textures → to_threejs → dict with maps.""" + from pymat import Material, vis + from pymat.vis.adapters import to_threejs + + results = vis.search(category="metal", limit=1) + if not results: + pytest.skip("No metal materials in mat-vis index") + + source = results[0].get("source", "ambientcg") + mat_id = results[0]["id"] + + m = Material(name="Live Metal Test") + m.vis.metallic = 1.0 + m.vis.roughness = 0.3 + m.vis.base_color = (0.8, 0.8, 0.8, 1.0) + m.vis.source_id = f"{source}/{mat_id}" + + d = to_threejs(m) + + # Scalars present + assert d["metalness"] == 1.0 + assert d["roughness"] == 0.3 + + # At least one texture map as base64 data URI + has_map = any( + k in d for k in ("map", "normalMap", "roughnessMap", "metalnessMap", "aoMap") + ) + assert has_map, f"No texture maps in to_threejs output: {list(d.keys())}" + + # Verify data URI format + for key in ("map", "normalMap", "roughnessMap", "metalnessMap", "aoMap"): + if key in d: + assert d[key].startswith("data:image/png;base64,"), f"{key}: not a data URI" + + def test_toml_material_with_vis_mapping(self): + """Stainless steel from TOML has vis.source_id from [vis] section.""" + from pymat import stainless + + assert stainless.vis.source_id is not None + assert stainless.vis.finish == "brushed" + assert stainless.vis.roughness == 0.3 + assert stainless.vis.metallic == 1.0 + + # Finishes available + assert "polished" in stainless.vis.finishes + + # Switch finish + stainless.vis.finish = "polished" + assert stainless.vis.source_id == "ambientcg/Metal012" + + # Switch back + stainless.vis.finish = "brushed" + + def test_discover_finds_candidates(self): + """vis.search() finds materials by category.""" + from pymat import vis + + # Use module-level search (tier-free) instead of discover() + # which delegates to mat_vis_client.search (tier-filtered) + results = vis.search(category="metal", limit=5) + assert len(results) > 0 + assert all("id" in c for c in results) + + def test_prefetch_small(self, tmp_path): + """vis.fetch works for multiple materials (light prefetch test).""" + from pymat import vis + + # Fetch just 2 materials instead of full prefetch + results = vis.search(category="stone", limit=2) + for r in results: + textures = vis.fetch(r["source"], r["id"], tier="128") + assert len(textures) >= 0 # may be 0 if 128 tier not available + + def test_resolve_channel(self): + """Vis.resolve() returns texture or scalar fallback.""" + from pymat import Material, vis + + results = vis.search(category="stone", limit=1) + if not results: + pytest.skip("No stone materials") + + source = results[0].get("source", "ambientcg") + mat_id = results[0]["id"] + + m = Material(name="Test Stone") + m.vis.roughness = 0.7 + m.vis.source_id = f"{source}/{mat_id}" + + rc = m.vis.resolve("roughness", scalar=0.7) + # Should have texture (if roughness map exists) or scalar fallback + assert rc.scalar == 0.7 + if rc.has_texture: + assert rc.texture[:4] == b"\x89PNG" diff --git a/tests/test_loader.py b/tests/test_loader.py index 66dc23d..244781a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -201,7 +201,9 @@ def test_alloy_compositions_sum_to_one(self): for name, mat in materials.items(): if mat.composition: total = sum(mat.composition.values()) - assert abs(total - 1.0) < 0.02, ( + # Handle ufloat values (uncertainties package) + nominal = getattr(total, "nominal_value", total) + assert abs(nominal - 1.0) < 0.02, ( f"{name}: composition sums to {total}, expected ~1.0" ) diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py index 2ac2025..7fa7138 100644 --- a/tests/test_readme_examples.py +++ b/tests/test_readme_examples.py @@ -78,6 +78,78 @@ def test_applying_to_shapes(self): assert box.mass > 0 assert box.color is not None + def test_molar_mass_from_formula(self): + """ + ## Computing Molar Mass + + `Material.molar_mass` is a computed property that parses the + chemical formula and looks up each element's atomic weight. + It supports fractional stoichiometry and strips dopant + notation like `LYSO:Ce` so doped-crystal aliases work + unchanged. + + Nothing is stored — it recomputes on each access. That's + intentional: molar mass is definitionally derived from + `formula` and should never drift. Missing or unknown-element + formulas return `None`. See + `docs/decisions/0001-derived-chemistry-properties-live-on-material.md`. + """ + from pymat import Material + + # Pure element + iron = Material(name="Iron", formula="Fe") + assert iron.molar_mass == 55.85 + + # Simple compound + alumina = Material(name="Alumina", formula="Al2O3") + assert abs(alumina.molar_mass - 101.96) < 0.01 + + # Fractional stoichiometry (a PET-scanner scintillator) + lyso = Material(name="LYSO", formula="Lu1.8Y0.2SiO5") + assert abs(lyso.molar_mass - 440.87) < 0.1 + + # Dopant suffix is stripped + lyso_ce = Material(name="LYSO:Ce", formula="Lu1.8Y0.2SiO5:Ce") + assert lyso_ce.molar_mass == lyso.molar_mass + + # Unit-aware companion accessor (Pint Quantity) + qty = iron.molar_mass_qty + assert qty.to("kg/mol").magnitude == pytest.approx(0.05585, abs=1e-4) + + # Gracefully returns None when no formula is set + unknown = Material(name="Unknown Alloy") + assert unknown.molar_mass is None + + def test_elements_low_level_api(self): + """ + ## Low-level: `pymat.elements` + + For callers that don't need a full `Material` object — + e.g. quick stoichiometry calculations inside a Monte Carlo + transport loop — the low-level `pymat.elements` module + exposes the same machinery directly. + + The `ATOMIC_WEIGHT` table is a line-for-line mirror of the + Rust `rs-materials` crate, so Python and Rust Monte Carlo + engines get identical molar masses byte-for-byte. + """ + from pymat.elements import ( + ATOMIC_WEIGHT, + compute_molar_mass, + parse_formula, + ) + + # Atomic weight lookup + assert ATOMIC_WEIGHT["Fe"] == 55.85 + assert ATOMIC_WEIGHT["Lu"] == 175.0 + + # Formula parser: fractional stoichiometry + repeat handling + counts = parse_formula("Lu1.8Y0.2SiO5") + assert counts == {"Lu": 1.8, "Y": 0.2, "Si": 1.0, "O": 5.0} + + # Molar mass directly from a formula string + assert abs(compute_molar_mass("Al2O3") - 101.96) < 0.01 + class TestHierarchicalMaterials: """Examples for hierarchical material definitions.""" diff --git a/tests/test_uncertainties.py b/tests/test_uncertainties.py new file mode 100644 index 0000000..111c45e --- /dev/null +++ b/tests/test_uncertainties.py @@ -0,0 +1,110 @@ +"""Tests for uncertainty/range support in composition and scalar values.""" + +from __future__ import annotations + +from uncertainties import UFloat, ufloat + +from pymat.loader import _parse_value, _parse_composition + + +class TestParseValue: + def test_plain_float(self): + assert _parse_value(0.5) == 0.5 + assert isinstance(_parse_value(0.5), float) + + def test_plain_int(self): + assert _parse_value(1) == 1.0 + + def test_dict_with_stddev(self): + v = _parse_value({"nominal": 0.4, "stddev": 0.1}) + assert isinstance(v, UFloat) + assert v.nominal_value == 0.4 + assert v.std_dev == 0.1 + + def test_dict_with_range(self): + v = _parse_value({"min": 0.2, "max": 0.6}) + assert isinstance(v, UFloat) + assert v.nominal_value == 0.4 # midpoint + assert abs(v.std_dev - 0.2) < 1e-10 # half-range + + def test_dict_with_nominal_and_range(self): + v = _parse_value({"nominal": 0.35, "min": 0.2, "max": 0.6}) + assert isinstance(v, UFloat) + assert v.nominal_value == 0.35 # explicit nominal, not midpoint + assert abs(v.std_dev - 0.2) < 1e-10 + + def test_dict_nominal_only(self): + v = _parse_value({"nominal": 0.5}) + assert v == 0.5 + assert isinstance(v, float) # no uncertainty info → plain float + + def test_dict_max_only(self): + # {nominal = 0.05, max = 0.1} — common for trace elements + v = _parse_value({"nominal": 0.05, "max": 0.1}) + assert v == 0.05 # no min → no range → plain float + + +class TestParseComposition: + def test_none(self): + assert _parse_composition(None) is None + + def test_plain_dict(self): + comp = _parse_composition({"Fe": 0.68, "Cr": 0.18}) + assert comp["Fe"] == 0.68 + assert isinstance(comp["Fe"], float) + + def test_mixed_dict(self): + comp = _parse_composition({ + "Fe": 0.68, + "Si": {"min": 0.2, "max": 0.6}, + "Cr": {"nominal": 0.18, "stddev": 0.02}, + }) + assert isinstance(comp["Fe"], float) + assert isinstance(comp["Si"], UFloat) + assert isinstance(comp["Cr"], UFloat) + assert comp["Si"].nominal_value == 0.4 + assert comp["Cr"].std_dev == 0.02 + + +class TestUncertaintyPropagation: + def test_ufloat_sum(self): + """Summing ufloats propagates uncertainty correctly.""" + a = ufloat(0.4, 0.1) + b = ufloat(0.6, 0.2) + total = a + b + assert total.nominal_value == 1.0 + # Uncertainty adds in quadrature + assert abs(total.std_dev - (0.1**2 + 0.2**2) ** 0.5) < 1e-10 + + def test_ufloat_mixed_with_float(self): + """ufloat + plain float works.""" + a = ufloat(0.4, 0.1) + b = 0.5 + total = a + b + assert total.nominal_value == 0.9 + assert total.std_dev == 0.1 # float adds zero uncertainty + + +class TestMaterialWithRanges: + def test_a6063_has_ufloat_composition(self): + from pymat import aluminum + + a6063 = aluminum.a6063 + assert a6063.composition is not None + + # Si has a range + si = a6063.composition["Si"] + assert isinstance(si, UFloat) + assert si.nominal_value > 0 + + # Al is plain float (balance) + al = a6063.composition["Al"] + assert isinstance(al, float) + + def test_a6061_still_plain_floats(self): + """Existing plain-float compositions are unaffected.""" + from pymat import aluminum + + a6061 = aluminum.a6061 + for el, val in a6061.composition.items(): + assert isinstance(val, float), f"{el} should be float, got {type(val)}" diff --git a/tests/test_vis.py b/tests/test_vis.py new file mode 100644 index 0000000..cacfa4c --- /dev/null +++ b/tests/test_vis.py @@ -0,0 +1,304 @@ +"""Tests for the pymat.vis module — model, wiring, adapters.""" + +from __future__ import annotations + +import pytest + +from pymat.vis._model import ResolvedChannel, Vis + + +# ── Vis construction ────────────────────────────────────────── + + +class TestVisConstruction: + def test_empty_vis(self): + v = Vis() + assert v.source_id is None + assert v.finish is None + assert v.finishes == {} + assert v.textures == {} + assert v.tier == "1k" + + def test_from_toml_with_default(self): + v = Vis.from_toml( + { + "default": "brushed", + "finishes": { + "brushed": "ambientcg/Metal032", + "polished": "ambientcg/Metal012", + }, + } + ) + assert v.source_id == "ambientcg/Metal032" + assert v.finish == "brushed" + assert v.finishes == { + "brushed": "ambientcg/Metal032", + "polished": "ambientcg/Metal012", + } + + def test_from_toml_no_default_uses_first(self): + v = Vis.from_toml( + { + "finishes": { + "matte": "polyhaven/metal_matte", + "satin": "polyhaven/metal_satin", + } + } + ) + assert v.finish == "matte" + assert v.source_id == "polyhaven/metal_matte" + + def test_from_toml_empty(self): + v = Vis.from_toml({}) + assert v.source_id is None + assert v.finishes == {} + + +# ── Finish switching ────────────────────────────────────────── + + +class TestFinishSwitching: + def test_switch_finish(self): + v = Vis.from_toml( + { + "default": "brushed", + "finishes": { + "brushed": "ambientcg/Metal032", + "polished": "ambientcg/Metal012", + }, + } + ) + assert v.source_id == "ambientcg/Metal032" + + v.finish = "polished" + assert v.source_id == "ambientcg/Metal012" + assert v.finish == "polished" + + def test_switch_clears_cache(self): + v = Vis.from_toml( + { + "default": "a", + "finishes": {"a": "src/a", "b": "src/b"}, + } + ) + # Simulate cached textures + v._textures = {"color": b"fake_png"} + v._fetched = True + + v.finish = "b" + assert v._textures == {} + assert v._fetched is False + + def test_switch_unknown_finish_raises(self): + v = Vis.from_toml( + { + "default": "brushed", + "finishes": {"brushed": "ambientcg/Metal032"}, + } + ) + with pytest.raises(ValueError, match="Unknown finish 'mirror'"): + v.finish = "mirror" + + +# ── Textures access ────────────────────────────────────────── + + +class TestTextures: + def test_no_source_id_returns_empty(self): + v = Vis() + assert v.textures == {} + + def test_with_source_id_attempts_fetch(self, monkeypatch): + """Verify that accessing textures triggers the fetch layer.""" + called = {} + + def mock_fetch(source, material_id, *, tier="1k", **kw): + called["source"] = source + called["material_id"] = material_id + return {"color": b"\x89PNG_mock"} + + monkeypatch.setattr("mat_vis_client.fetch", mock_fetch) + + v = Vis(source_id="ambientcg/Metal032") + textures = v.textures + assert called["source"] == "ambientcg" + assert called["material_id"] == "Metal032" + assert textures["color"] == b"\x89PNG_mock" + + +# ── ResolvedChannel ────────────────────────────────────────── + + +class TestResolvedChannel: + def test_texture_available(self): + v = Vis() + v._textures = {"roughness": b"\x89PNG_roughness"} + v._fetched = True + v.source_id = "test/id" # set to avoid re-fetch + + rc = v.resolve("roughness", scalar=0.3) + assert rc.has_texture is True + assert rc.texture == b"\x89PNG_roughness" + assert rc.scalar == 0.3 + + def test_texture_missing_fallback_to_scalar(self): + v = Vis() + v._textures = {} + v._fetched = True + v.source_id = "test/id" + + rc = v.resolve("metalness", scalar=1.0) + assert rc.has_texture is False + assert rc.texture is None + assert rc.scalar == 1.0 + + def test_no_texture_no_scalar(self): + v = Vis() + # source_id is None → textures returns {} + rc = v.resolve("color") + assert rc.has_texture is False + assert rc.texture is None + assert rc.scalar is None + + +# ── Discover ───────────────────────────────────────────────── + + +class TestDiscover: + def test_discover_returns_candidates(self, monkeypatch): + import mat_vis_client as _client + + mock_results = [ + {"id": "Metal032", "source": "ambientcg", "category": "metal", "score": 0.1}, + {"id": "Metal012", "source": "ambientcg", "category": "metal", "score": 0.3}, + ] + monkeypatch.setattr(_client, "search", lambda **kw: mock_results) + + v = Vis() + candidates = v.discover(category="metal") + assert len(candidates) == 2 + assert candidates[0]["id"] == "ambientcg/Metal032" + assert v.source_id is None # not set without auto_set + + def test_discover_auto_set(self, monkeypatch): + import mat_vis_client as _client + + mock_results = [ + {"id": "Metal032", "source": "ambientcg", "category": "metal", "score": 0.1}, + ] + monkeypatch.setattr(_client, "search", lambda **kw: mock_results) + + v = Vis() + v.discover(category="metal", auto_set=True) + assert v.source_id == "ambientcg/Metal032" + + def test_discover_no_results(self, monkeypatch): + import mat_vis_client as _client + + monkeypatch.setattr(_client, "search", lambda **kw: []) + + v = Vis() + candidates = v.discover(category="exotic") + assert candidates == [] + assert v.source_id is None + + +# ── Material.vis wiring ────────────────────────────────────── + + +class TestMaterialVisWiring: + def test_custom_material_gets_empty_vis(self): + from pymat import Material + + m = Material(name="test-alloy", density=5.0) + assert m.vis is not None + assert m.vis.source_id is None + assert m.vis.textures == {} + + def test_vis_is_settable(self): + from pymat import Material + + m = Material(name="test") + m.vis.source_id = "ambientcg/Wood001" + assert m.vis.source_id == "ambientcg/Wood001" + + def test_vis_same_instance_on_repeat_access(self): + from pymat import Material + + m = Material(name="test") + v1 = m.vis + v2 = m.vis + assert v1 is v2 + + def test_toml_material_gets_populated_vis(self): + from pymat import stainless + + assert stainless.vis.source_id is not None + assert stainless.vis.finish == "brushed" + assert "polished" in stainless.vis.finishes + + def test_child_without_vis_gets_empty(self): + from pymat import stainless + + # s304 has no [vis] section — gets empty Vis + s304 = stainless.s304 + assert s304.vis.source_id is None + + +# ── Module-level API ───────────────────────────────────────── + + +class TestVisModuleApi: + def test_import_vis(self): + from pymat import vis + + assert hasattr(vis, "search") + assert hasattr(vis, "fetch") + assert hasattr(vis, "rowmap_entry") + assert hasattr(vis, "get_manifest") + + def test_get_manifest_returns_dict(self): + from pymat import vis + + manifest = vis.get_manifest() + assert "release_tag" in manifest + assert "tiers" in manifest + + def test_search_with_mock(self, monkeypatch): + """Search against a mock client (no network).""" + from pymat import vis + import mat_vis_client as _client + + mock_results = [ + {"id": "Metal001", "source": "ambientcg", "category": "metal", "roughness": 0.3, "metalness": 1.0}, + ] + + class MockClient: + def __init__(self, **kw): pass + @property + def manifest(self): return {"release_tag": "mock", "tiers": {}} + def sources(self, tier="1k"): return ["ambientcg"] + def index(self, source): return mock_results + def search(self, **kw): return mock_results + + monkeypatch.setattr(_client, "_client", MockClient()) + + results = vis.search(category="metal") + assert len(results) >= 1 + assert results[0]["id"] == "Metal001" + + def test_rowmap_entry_missing_material_raises(self, monkeypatch): + from pymat import vis + import mat_vis_client as _client + + class MockClient: + def __init__(self, **kw): pass + @property + def manifest(self): return {"release_tag": "mock", "tiers": {}} + def rowmap_entry(self, source, mid, **kw): + raise KeyError(f"NotExist") + + monkeypatch.setattr(_client, "_client", MockClient()) + + with pytest.raises(KeyError, match="NotExist"): + vis.rowmap_entry("ambientcg", "NotExist") diff --git a/tests/test_visual_regression.py b/tests/test_visual_regression.py new file mode 100644 index 0000000..be3b82b --- /dev/null +++ b/tests/test_visual_regression.py @@ -0,0 +1,257 @@ +"""Visual regression tests — headless Three.js rendering via Playwright. + +Proves the full pipeline: + pymat.Material → .vis → to_threejs → build123d export_gltf → Three.js render → screenshot + +Uses a self-contained HTML viewer (tests/visual_render.html) that loads +Three.js from CDN + our exported glTF. No ocp_vscode needed. + +Requirements: + pip install playwright build123d + python -m playwright install chromium + +Skip with: MAT_VIS_SKIP_VISUAL=1 (default) +Run: MAT_VIS_SKIP_VISUAL=0 pytest tests/test_visual_regression.py -v -s +""" + +from __future__ import annotations + +import http.server +import json +import os +import threading +import time +from pathlib import Path + +import pytest + +SKIP_VISUAL = os.environ.get("MAT_VIS_SKIP_VISUAL", "1") == "1" +OUTPUT_DIR = Path(__file__).parent / "visual_output" +RENDER_HTML = Path(__file__).parent / "visual_render.html" + + +@pytest.fixture(scope="module") +def file_server(): + """Serve test files (HTML + glTF) on localhost.""" + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(OUTPUT_DIR), **kwargs) + + def log_message(self, *args): + pass # silence + + # Copy render HTML to output dir + import shutil + shutil.copy(RENDER_HTML, OUTPUT_DIR / "index.html") + + port = 8765 + server = http.server.HTTPServer(("127.0.0.1", port), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + yield f"http://127.0.0.1:{port}" + server.shutdown() + + +@pytest.fixture(scope="module") +def browser(): + """Launch headless Chromium.""" + try: + from playwright.sync_api import sync_playwright + except ImportError: + pytest.skip("playwright not installed") + + pw = sync_playwright().start() + b = pw.chromium.launch(headless=True, args=["--use-gl=swiftshader"]) + yield b + b.close() + pw.stop() + + +def _render_and_screenshot(browser, server_url: str, glb_path: Path, name: str) -> Path: + """Load a glTF in the Three.js viewer, wait for render, extract canvas.""" + import base64 + + out = OUTPUT_DIR / f"{name}.png" + + page = browser.new_page(viewport={"width": 800, "height": 600}) + page.goto(f"{server_url}/index.html?gltf={glb_path.name}") + + # Wait for Three.js to signal render complete + page.wait_for_function("window.__renderComplete === true", timeout=15000) + page.wait_for_timeout(500) + + # Extract canvas content directly (more reliable than page screenshot) + data_url = page.evaluate('document.querySelector("canvas").toDataURL("image/png")') + png_bytes = base64.b64decode(data_url.split(",")[1]) + out.write_bytes(png_bytes) + + page.close() + return out + + +@pytest.mark.skipif(SKIP_VISUAL, reason="MAT_VIS_SKIP_VISUAL=1 (default)") +class TestHeadlessRender: + """Full pipeline: Material → glTF → Three.js headless → screenshot.""" + + def test_steel_cube(self, file_server, browser): + """Metallic steel cube with PBR scalars.""" + from build123d import Box, export_gltf + from pymat import Material + + shape = Box(10, 10, 10) + m = Material(name="Steel") + m.vis.metallic = 1.0 + m.vis.roughness = 0.3 + m.vis.base_color = (0.8, 0.8, 0.8, 1.0) + shape.material = m + + glb = OUTPUT_DIR / "steel_cube.glb" + export_gltf(shape, str(glb)) + assert glb.exists() + + screenshot = _render_and_screenshot(browser, file_server, glb, "steel_cube") + assert screenshot.exists() + size = screenshot.stat().st_size + assert size > 5000, f"Screenshot too small ({size} bytes) — likely blank" + print(f"steel_cube: {size} bytes") + + def test_red_sphere(self, file_server, browser): + """Red dielectric sphere.""" + from build123d import Sphere, export_gltf + from pymat import Material + + shape = Sphere(8) + m = Material(name="Red Plastic") + m.vis.metallic = 0.0 + m.vis.roughness = 0.5 + m.vis.base_color = (0.8, 0.1, 0.1, 1.0) + shape.material = m + + glb = OUTPUT_DIR / "red_sphere.glb" + export_gltf(shape, str(glb)) + + screenshot = _render_and_screenshot(browser, file_server, glb, "red_sphere") + assert screenshot.exists() + size = screenshot.stat().st_size + assert size > 5000, f"Screenshot too small ({size} bytes)" + print(f"red_sphere: {size} bytes") + + def test_gold_cylinder(self, file_server, browser): + """Gold metallic cylinder.""" + from build123d import Cylinder, export_gltf + from pymat import Material + + shape = Cylinder(5, 15) + m = Material(name="Gold") + m.vis.metallic = 1.0 + m.vis.roughness = 0.2 + m.vis.base_color = (1.0, 0.84, 0.0, 1.0) + shape.material = m + + glb = OUTPUT_DIR / "gold_cylinder.glb" + export_gltf(shape, str(glb)) + + screenshot = _render_and_screenshot(browser, file_server, glb, "gold_cylinder") + assert screenshot.exists() + size = screenshot.stat().st_size + assert size > 5000 + print(f"gold_cylinder: {size} bytes") + + def test_glass_transmission(self, file_server, browser): + """Transparent glass sphere.""" + from build123d import Sphere, export_gltf + from pymat import Material + + shape = Sphere(10) + m = Material(name="Glass") + m.vis.metallic = 0.0 + m.vis.roughness = 0.0 + m.vis.base_color = (0.95, 0.95, 1.0, 0.3) + m.vis.ior = 1.52 + m.vis.transmission = 0.9 + shape.material = m + + glb = OUTPUT_DIR / "glass_sphere.glb" + export_gltf(shape, str(glb)) + + screenshot = _render_and_screenshot(browser, file_server, glb, "glass_sphere") + assert screenshot.exists() + size = screenshot.stat().st_size + assert size > 3000 + print(f"glass_sphere: {size} bytes") + + def test_multi_material_assembly(self, file_server, browser): + """Assembly with different materials per part.""" + from build123d import Box, Cylinder, Pos, Compound, export_gltf + from pymat import Material + + base = Box(20, 20, 3) + m_base = Material(name="Wood Base") + m_base.vis.metallic = 0.0 + m_base.vis.roughness = 0.7 + m_base.vis.base_color = (0.6, 0.4, 0.2, 1.0) + base.material = m_base + + pin = Pos(0, 0, 10) * Cylinder(3, 14) + m_pin = Material(name="Chrome Pin") + m_pin.vis.metallic = 1.0 + m_pin.vis.roughness = 0.1 + m_pin.vis.base_color = (0.9, 0.9, 0.9, 1.0) + pin.material = m_pin + + assembly = Compound(children=[base, pin]) + + glb = OUTPUT_DIR / "assembly.glb" + export_gltf(assembly, str(glb)) + + screenshot = _render_and_screenshot(browser, file_server, glb, "assembly") + assert screenshot.exists() + size = screenshot.stat().st_size + assert size > 5000 + print(f"assembly: {size} bytes") + + +@pytest.mark.skipif(SKIP_VISUAL, reason="MAT_VIS_SKIP_VISUAL=1 (default)") +class TestAdapterOutput: + """Verify adapter dict output (no rendering needed).""" + + def test_to_threejs_scalars(self): + from pymat import Material + from pymat.vis.adapters import to_threejs + + m = Material(name="Steel") + m.vis.metallic = 1.0 + m.vis.roughness = 0.3 + m.vis.base_color = (0.8, 0.8, 0.8, 1.0) + + d = to_threejs(m) + assert d["metalness"] == 1.0 + assert d["roughness"] == 0.3 + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + (OUTPUT_DIR / "steel_threejs.json").write_text(json.dumps(d, indent=2, default=str)) + + def test_to_threejs_with_textures(self): + from pymat import Material, vis + from pymat.vis.adapters import to_threejs + + results = vis.search(category="metal", limit=1) + if not results: + pytest.skip("No metals in mat-vis") + + m = Material(name="Textured Metal") + m.vis.metallic = 1.0 + m.vis.roughness = 0.3 + m.vis.source_id = f"{results[0]['source']}/{results[0]['id']}" + + d = to_threejs(m) + has_map = any(k in d for k in ("map", "normalMap", "roughnessMap", "metalnessMap")) + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + summary = {k: (v[:60] + "..." if isinstance(v, str) and len(v) > 60 else v) for k, v in d.items()} + (OUTPUT_DIR / "metal_textured_threejs.json").write_text(json.dumps(summary, indent=2, default=str)) + + if has_map: + assert d[next(k for k in ("map", "normalMap") if k in d)].startswith("data:image/png;base64,") diff --git a/tests/visual_output/metal_textured_threejs.json b/tests/visual_output/metal_textured_threejs.json new file mode 100644 index 0000000..ed9195b --- /dev/null +++ b/tests/visual_output/metal_textured_threejs.json @@ -0,0 +1,12 @@ +{ + "type": "MeshPhysicalMaterial", + "metalness": 1.0, + "roughness": 0.3, + "ior": 1.5, + "transmission": 0.0, + "map": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQAEAIAAA...", + "normalMap": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAAAAA...", + "roughnessMap": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAAAAA...", + "metalnessMap": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAAAAA...", + "aoMap": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQAEAAAAA..." +} \ No newline at end of file diff --git a/tests/visual_output/steel_threejs.json b/tests/visual_output/steel_threejs.json new file mode 100644 index 0000000..ae8eebe --- /dev/null +++ b/tests/visual_output/steel_threejs.json @@ -0,0 +1,7 @@ +{ + "type": "MeshPhysicalMaterial", + "metalness": 1.0, + "roughness": 0.3, + "ior": 1.5, + "transmission": 0.0 +} \ No newline at end of file diff --git a/tests/visual_render.html b/tests/visual_render.html new file mode 100644 index 0000000..8a004c8 --- /dev/null +++ b/tests/visual_render.html @@ -0,0 +1,96 @@ + + + + + + + + + +