Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

173 Commits

Repository files navigation

pymat

A hierarchical material library for CAD applications and Monte Carlo particle transport, with build123d integration.

Features

  • 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 via material.vis: Scalars (roughness, metallic, base_color) and lazy-fetched textures from mat-vis
  • 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 vis (visualization)
  • Python 3.10 – 3.13 supported, core library depends only on pint

Installation

# From PyPI (recommended)
pip install py-materials
# or: uv add py-materials# 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

Creating Materials

Create materials with convenient parameters:

frompymatimportMaterial# Using convenience parameterssteel=Material(name="Steel", density=7.85)
assertsteel.density==7.85# With visualization coloraluminum=Material(name="Aluminum", density=2.7, color=(0.88, 0.88, 0.88))
assertaluminum.vis.base_color[:3] == (0.88, 0.88, 0.88)
# With formulalyso=Material(name="LYSO", formula="Lu1.8Y0.2SiO5", density=7.1)
assertlyso.formula=="Lu1.8Y0.2SiO5"

Using Property Groups

Define multiple properties at once using property group dictionaries:

frompymatimportMaterial# Define steel with multiple property groupssteel=Material(
name="Stainless Steel 304",
mechanical={"density": 8.0, "youngs_modulus": 193, "yield_strength": 170},
thermal={"melting_point": 1450, "thermal_conductivity": 15.1},
vis={"base_color": (0.75, 0.75, 0.77, 1.0), "metallic": 1.0},
)
assertsteel.properties.mechanical.density==8.0assertsteel.properties.mechanical.youngs_modulus==193assertsteel.properties.thermal.melting_point==1450assertsteel.vis.metallic==1.0

Applying Materials to Shapes

Apply materials to build123d shapes for visualization and mass calculation:

frombuild123dimportBoxfrompymatimportMaterial# Create materialsteel=Material(name="Steel", density=7.85, color=(0.7, 0.7, 0.7))
# Create shape and apply materialbox=Box(10, 10, 10)
steel.apply_to(box)
assertbox.material.name=="Steel"assertbox.mass>0assertbox.colorisnotNone

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.

frompymatimportMaterial# Pure elementiron=Material(name="Iron", formula="Fe")
assertiron.molar_mass==55.85# Simple compoundalumina=Material(name="Alumina", formula="Al2O3")
assertabs(alumina.molar_mass-101.96) <0.01# Fractional stoichiometry (a PET-scanner scintillator)lyso=Material(name="LYSO", formula="Lu1.8Y0.2SiO5")
assertabs(lyso.molar_mass-440.87) <0.1# Dopant suffix is strippedlyso_ce=Material(name="LYSO:Ce", formula="Lu1.8Y0.2SiO5:Ce")
assertlyso_ce.molar_mass==lyso.molar_mass# Unit-aware companion accessor (Pint Quantity)qty=iron.molar_mass_qtyassertqty.to("kg/mol").magnitude==pytest.approx(0.05585, abs=1e-4)
# Gracefully returns None when no formula is setunknown=Material(name="Unknown Alloy")
assertunknown.molar_massisNone

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.

frompymat.elementsimport (
ATOMIC_WEIGHT,
compute_molar_mass,
parse_formula,
)
# Atomic weight lookupassertATOMIC_WEIGHT["Fe"] ==55.85assertATOMIC_WEIGHT["Lu"] ==175.0# Formula parser: fractional stoichiometry + repeat handlingcounts=parse_formula("Lu1.8Y0.2SiO5")
assertcounts== {"Lu": 1.8, "Y": 0.2, "Si": 1.0, "O": 5.0}
# Molar mass directly from a formula stringassertabs(compute_molar_mass("Al2O3") -101.96) <0.01

Chainable Material Hierarchy

Build hierarchies with grades, tempers, and treatments:

frompymatimportMaterial# Create base stainless steelstainless=Material(name="Stainless Steel", density=8.0, thermal={"melting_point": 1450})
# Add grades304=stainless.grade_("304", name="SS 304", mechanical={"yield_strength": 170})
asserts304.density==8.0# Inheritedasserts304.properties.mechanical.yield_strength==170# Add treatmentpassivated=s304.treatment_("passivated", name="SS 304 Passivated")
assert (
passivated.path=="stainless_steel.304.passivated"
) # name -> lowercase with underscoresassertpassivated.density==8.0# Inherited through chain

Direct Material Access

Load materials and access them directly from the library:

frompymatimportaluminum, lyso, stainless# Direct access to materialss316L=stainless.s316Lasserts316L.grade=="316L"al6061=aluminum.a6061assertal6061.density==2.7# Inherited from aluminumlyso_crystal=lysoassert"LYSO"inlyso_crystal.name

Customizing Appearance Safely

Materials reached via pymat["..."] or category imports (from pymat import stainless) are shared instances — the same object every caller in the process sees. Mutating m.vis.roughness = 0.6 or flipping m.vis.finish on those leaks into every other consumer.

The safe pattern uses two methods that pair cleanly:

importpymatfrompymat.visimportto_threejssteel=pymat["Stainless Steel 304"] # registry singleton — DON'T mutate# Derive an independent variant (1) and attach it to a fresh Material (2)polished_vis=steel.vis.override(roughness=0.05, finish="polished")
shiny=steel.with_vis(polished_vis)
three=to_threejs(shiny) # uses the polished valuesassertthree["roughness"] ==0.05assertsteel.vis.material_id=="Metal012"# registry untouched# ``Vis.override(**deltas) -> Vis`` → deep-copied Vis with deltas# (finishes deep-copied; identity changes invalidate the texture# cache atomically; unknown kwargs raise TypeError).# ``Material.with_vis(vis) -> Material`` → registry-detached Material# with the supplied Vis attached. Equivalent to ``Material.copy()``# followed by slotting the new vis in.# ``Material.copy() -> Material`` → generic detach if you need to# tweak more than just the Vis.# Materials *you* construct directly (Material(name="custom",# vis={...})) are not shared and can be mutated freely.

Physics Properties vs Visualization

Understand the difference between measured optical properties (physics) and rendering properties (visualization):

frompymatimportMaterial# Create transparent materialglass=Material(
name="Optical Glass",
color=(0.9, 0.9, 0.9, 0.8), # Visual: 80% opaque whiteoptical={"transparency": 95, "refractive_index": 1.517}, # Physics: 95% transmissionvis={"transmission": 0.8}, # Rendering: how transparent it looks
)
# Physics properties (measured)assertglass.properties.optical.transparency==95assertglass.properties.optical.refractive_index==1.517# Visualization properties (rendering)assertglass.vis.base_color[3] ==0.8# Alphaassertglass.vis.transmission==0.8

Scintillator-Specific Properties

Define detector crystals with optical physics properties:

frompymatimportMateriallyso_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
},
vis={"base_color": (0.0, 1.0, 1.0, 0.85), "transmission": 0.85},
)
assertlyso_crystal.properties.optical.light_yield==32000assertlyso_crystal.properties.optical.decay_time==41assertlyso_crystal.vis.transmission==0.85

Temperature-Dependent Materials

Use factory functions for materials with properties that depend on external parameters:

frompymat.factoriesimportwater# Water at different temperaturescold_water=water(4) # Max densityroom_water=water(20) # Room temperaturehot_water=water(80) # Heatedassertcold_water.density>room_water.densityassertroom_water.density>hot_water.density# Verify realistic valuesassert0.99<cold_water.density<1.01assert0.95<hot_water.density<0.98

Air at Different Conditions

Create air material at specific temperature and pressure:

frompymat.factoriesimportairsea_level=air(15, 1.0) # 15°C, 1 atmhigh_altitude=air(15, 0.5) # 15°C, 0.5 atm (5500m)assertsea_level.density>high_altitude.density

Saline Solutions

Create solutions with specific concentration and temperature:

frompymat.factoriesimportsaline, water# Physiological saline at body temperaturephantom=saline(0.9, temperature_c=37)
# Saline is slightly denser than pure water at same temperaturepure_water_37=water(37)
assertphantom.density>pure_water_37.density# Seawater (3.5% NaCl) at 20°Cseawater=saline(3.5, temperature_c=20)
# Higher concentration = higher densityassertseawater.density>phantom.density

Loading Metal Materials

Access various metal materials from the metals category:

frompymatimportaluminum, copper, stainless# Stainless steel variantss304=stainless.s304s316L=stainless.s316Lasserts304.density==s316L.density# Same base density# Aluminum alloysal6061=aluminum.a6061_=aluminum.a7075assertal6061.density==2.7# Coppercopper_material=copperassertcopper_material.density==8.96

Plastic Materials

Access plastic materials for 3D printing and engineering:

frompymatimportpc, peek, pla, pmma# Engineering plasticsassertpeek.properties.manufacturing.print_nozzle_temp==360# 3D printing plasticsassertpla.properties.manufacturing.printable_fdmisTrue# Transparent plasticsassertpmma.properties.optical.transparency==92assertpc.properties.optical.transparency==89

Scintillator Crystals

Access scintillator materials for radiation detectors:

frompymatimportbgo, lyso, nai# LYSO crystalassertlyso.properties.optical.light_yield==32000assertlyso.properties.optical.refractive_index==1.82# BGO crystalassertbgo.properties.optical.light_yield==8500# NaI crystalassertnai.properties.optical.light_yield==38000

Gas Materials

Access gases for simulation and detector design:

frompymatimportair, argon, helium, nitrogen, xenon# Common gases at STPassert0.0012<air.density<0.0013# g/cm³assertnitrogen.density>helium.density# Helium is lightestassertxenon.density>argon.density# Heavier noble gases# Detector gasesassertargon.properties.compliance.radiation_resistantisTrue

Property Inheritance

Child materials inherit properties from parents unless overridden:

frompymatimportMaterial# Create material hierarchyroot=Material(
name="Base", density=7.8, thermal={"melting_point": 1500, "thermal_conductivity": 50}
)
grade1=root.grade_("G1", mechanical={"yield_strength": 400})
assertgrade1.density==7.8# Inheritedassertgrade1.properties.mechanical.yield_strength==400# New propertyassertgrade1.properties.thermal.melting_point==1500# Inherited# Override inherited propertygrade2=root.grade_("G2", thermal={"melting_point": 1600})
assertgrade2.properties.thermal.melting_point==1600# Overridden

Automatic Mass Calculation

Materials with density automatically calculate shape mass:

frombuild123dimportBoxfrompymatimportaluminum, 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 gassert7.9<steel_box.mass<8.1# Aluminum boxal_box=Box(10, 10, 10)
aluminum.apply_to(al_box)
# Density = 2.7 g/cm³ → Mass = 2.7 gassert2.6<al_box.mass<2.8

Material Visualization

Materials render with appropriate colors for visualization:

frombuild123dimportBoxfrompymatimportaluminum, lyso, stainless# Create shapessteel_part=Box(10, 10, 10)
al_part=Box(10, 10, 10)
crystal=Box(10, 10, 10)
# Apply materialsstainless.apply_to(steel_part)
aluminum.apply_to(al_part)
lyso.apply_to(crystal)
# Verify colors are setassertsteel_part.colorisnotNoneassertal_part.colorisnotNoneassertcrystal.colorisnotNone# Colors should differassertsteel_part.color!=al_part.colorassertcrystal.color!=steel_part.color

Provenance — where every value comes from

Every curated value carries a _sources entry pointing at a primary paper or handbook. Material.source_of(path) returns the Source for a property; short aliases ("density") and fully-qualified paths ("optical.light_yield") both resolve.

frompymatimportbgo, inconel625# BGO's light yield comes from Weber & Monchamp's 1973 discovery paper.src=bgo.source_of("optical.light_yield")
assertsrc.kind=="doi"assertsrc.ref=="10.1063/1.1662183"assertsrc.license=="proprietary-reference-only"# Inconel 625's density traces to MIL-HDBK-5J Table 6.3.3.0(b).ms=inconel625.source_of("mechanical.density")
assertms.kind=="handbook"assertms.ref=="mil-hdbk-5j:p6-35"assertms.license=="PD-USGov"

Citing values in publications

Material.cite(path) returns a BibTeX entry for one property; Material.cite() (no arg) returns every source the material uses, deduplicated.

frompymatimportbgobib=bgo.cite("optical.light_yield")
assert"@"inbib# BibTeX entry type markerassert"10.1063/1.1662183"inbib# the DOI is embedded

Property values with uncertainty

Add a <prop>_stddev sibling key to a property and the loader folds it into a ufloat (from the uncertainties package) at load time. The build123d boundary still receives a plain float (the nominal value) — Material.density_g_mm3 strips the uncertainty so CAD math doesn't surprise downstream consumers.

frompymatimportload_tomlp=tmp_path/"steel.toml"p.write_text(
"[steel]\n"'name = "Steel"\n'"[steel.mechanical]\n""density_value = 7.85\n"'density_unit = "g/cm^3"\n'"density_stddev = 0.05\n"
)
steel=load_toml(p)["steel"]
d=steel.properties.mechanical.density# `d` is a ufloat — arithmetic on it propagates uncertaintyassertd.nominal_value==7.85assertd.std_dev==0.05# Plain-float view for build123d-style consumersassertsteel.density_g_mm3==0.00785# 7.85 g/cm³ → g/mm³

Composition with grade-spec ranges

Alloy specs (AMS, ASTM, EN AW) typically give a tolerance window per element. The loader accepts {nominal, min, max} per element and folds it into a ufloat whose ±σ spans the spec window.

frompymatimportaluminum# 6063 has min/max windows from the AZoM/ASM spec sheeta6063=aluminum.a6063# Si is 0.2-0.6 wt% (nominal 0.4); the loader stores it as a ufloatsi=a6063.composition["Si"]
assertsi.nominal_value==0.004# ±σ is half the spec windowassertsi.std_dev==0.002

Temperature-dependent properties

For values that vary with T, attach a <prop>_curve sibling with temps_K knots and values. Then call <prop>_at(T) on the property group with a Pint temperature Quantity. Linear interpolation between knots; values clamp at the boundaries.

frompymatimportload_tomlfrompymat.unitsimporturegp=tmp_path/"ofhc.toml"p.write_text(
"[copper]\n"'name = "Copper"\n'"[copper.thermal]\n""thermal_conductivity_value = 391\n"'thermal_conductivity_unit = "W/(m*K)"\n'"thermal_conductivity_curve = { ""temps_K = [77, 295, 500], ""values = [600, 391, 350] ""}\n"
)
copper=load_toml(p)["copper"]
# Scalar still reads as the RT valueassertcopper.properties.thermal.thermal_conductivity==391# _at(T) follows the curvek77=copper.properties.thermal.thermal_conductivity_at(77*ureg.kelvin)
assertk77.to("W/(m*K)").magnitude==600# Linear interp between 77 K and 295 Kk186=copper.properties.thermal.thermal_conductivity_at(186*ureg.kelvin)
assert495<k186.to("W/(m*K)").magnitude<496

Modern scintillators

The 3.11 release added GAGG:Ce, LSO:Ce, LaBr3:Ce, CeBr3, and SrI2:Eu, each cited to its primary measurement paper.

frompymatimportcebr3, gagg, sri2# GAGG:Ce — the dotted child carries the doped scalarsassertgagg.Ce.properties.optical.light_yield==40000assertgagg.Ce.properties.optical.decay_time==53# CeBr3 — fast, bright, intrinsic 4% resolution at 662 keVassertcebr3.density==5.2assertcebr3.properties.optical.light_yield==68000# SrI2:Eu — highest light yield in the catalogassertsri2.Eu.properties.optical.light_yield==115000

Aerospace nickel-base superalloys

Inconel 625 and 718 ship with MIL-HDBK-5J A-basis design allowables and Special Metals technical-bulletin thermal data.

frompymatimportinconel625, inconel718assertinconel625.density==8.44assertinconel625.properties.mechanical.tensile_strength==820# MPa, A-basisassertinconel718.density==8.22# Inconel 718 STA — much higher Ftu than 625 annealedassertinconel718.properties.mechanical.tensile_strength==1241

Material Categories

  • Metals: Stainless steel, aluminum, copper, tungsten, lead, titanium, brass
  • Scintillators: LYSO, BGO, NaI, CsI, LaBr3, PWO, plastic scintillators
  • Plastics: PEEK, Delrin, Ultem, PTFE, ESR, Nylon, PLA, ABS, PETG, TPU, PMMA, PE, PC
  • Ceramics: Alumina, Macor, Zirconia, Glass (borosilicate, fused silica, BK7)
  • Electronics: FR4, Rogers, Kapton, copper PCB, solder, ferrite
  • Liquids: Water, Heavy Water, Mineral Oil, Glycerol, Silicone Oil
  • Gases: Air, Nitrogen, Oxygen, Argon, CO₂, Helium, Hydrogen, Neon, Xenon, Methane, Vacuum

Property Groups

Each material can have properties organized in these domains:

  • Mechanical: density, Young's modulus, yield strength, hardness
  • Thermal: melting point, thermal conductivity, expansion coefficient
  • Electrical: resistivity, conductivity, dielectric constant
  • Optical: refractive index, transparency, light yield (PHYSICS - measured values)
  • Vis (on material.vis): base_color, metallic, roughness, ior, transmission, textures, finishes — visual/rendering layer, fetches PBR textures from mat-vis on demand
  • Manufacturing: machinability, printability, weldability
  • Compliance: RoHS, REACH, food-safe, biocompatible
  • Sourcing: cost, availability, suppliers

Advanced Usage

Custom Materials

Create materials using property group dictionaries:

frompymatimportMaterialmy_material=Material(
name="Custom Alloy",
mechanical={
"density": 8.1,
"youngs_modulus": 200,
"yield_strength": 450
},
vis={
"base_color": (0.7, 0.7, 0.75, 1.0),
"metallic": 1.0,
"roughness": 0.4
}
)

Loading from TOML

frompymatimportload_tomlmaterials=load_toml("my_materials.toml")
my_material=materials["my_material"]

Enrichment from Chemical Formulas

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).

frompymatimportMaterial, enrich_from_periodictable# Pure element — density is setiron=Material(name="Iron", formula="Fe")
enrich_from_periodictable(iron)
assertiron.density==7.874# set from periodictableassertiron.molar_mass==55.85# computed from formula# Compound — composition is set, density is notalumina=Material(name="Alumina", formula="Al2O3")
enrich_from_periodictable(alumina)
assertalumina.composition== {"Al": 2, "O": 3}
assertalumina.molar_mass==101.96# computed from formulaassertalumina.densityisNone# use enrich_from_matproj for compounds

Optical vs Visual Properties

Important: Understand the distinction between physics and visualization:

  • Optical Properties (properties.optical.*): Measured/calculated physical values

    • transparency: % light transmission (measured)
    • refractive_index: optical property (measured)
    • light_yield: scintillator brightness (measured)
  • Visual Properties (material.vis.*): Rendering/visualization parameters

    • base_color: RGBA values (0-1) for display
    • transmission: how transparent it LOOKS in renders
    • metallic: surface finish appearance
    • roughness: surface roughness appearance
    • source + material_id + tier: mat-vis appearance identity (since 3.1; the three fields match mat-vis-client's positional-arg signature — see ADR-0002)
    • textures: PBR texture maps (lazy-fetched from mat-vis once source + material_id are set)
    • finishes: named alternate looks (e.g. brushed / polished / oxidized) — each entry is an inline {source, id} table
    • mtlx (property): MaterialX document accessor (.xml, .export(path), .original)
    • client (property): escape hatch to the shared MatVisClient singleton — for any operation not material-keyed

These can differ intentionally! A material might be physically transparent (95% optical transmission) but rendered opaque (0% vis.transmission) for CAD clarity.

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.

Links

About

Hierarchical material library for CAD applications with build123d integration

Resources

Contributing

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages