# Functional Grading Guide ```{include} _guide-sidebar-compiler-membership.md ``` This guide is a practical reference for defining **spatial gradients** in OpenVCAD. Gradients are the core mechanism for creating functionally graded designs — parts whose material properties, colors, or compositions change continuously through space. This guide stays focused on the **expression-string** workflow because it is the shortest way to build many common gradients. If you want to write gradients as Python functions instead, annotate them with **Numba `@cfunc`** and use the [Python functions for attributes guide](numba-attributes.md). ## Prerequisites This guide assumes you have completed the [Getting Started Guide](getting-started.md), in particular: - **Lesson 4** (Attributes / `FloatAttribute`) - **Lesson 5** (Colors / `Vec4Attribute`) - **Lesson 6** (Volume Fractions / `VolumeFractionsAttribute`) If you have not worked through those lessons, please do so first. They introduce the attribute system and show how constant and simple gradient expressions are attached to geometry. For the canonical attribute name strings (`MODULUS`, `DENSITY`, `COLOR_RGBA`, `VOLUME_FRACTIONS`, etc.) and their `*_TYPE` helpers, see {ref}`Default attribute names and types ` in the Python API. ## Setup All scripts in this guide use **matplotlib** and **numpy** to plot the gradient functions alongside the 3D renders. Install them if you haven't already: ```bash pip install matplotlib numpy ``` ## Running the Examples Scripts are located in `examples/2_gradients/`. Run any script directly: ```bash python 01_linear_gradients.py ``` Each script will: 1. Open the interactive OpenVCAD render preview (close the window to continue). 2. Display a matplotlib plot of the gradient expressions used. > **Note:** To view an attribute in the render window, select it from **Object -> Selected Attribute**. --- ## Building Parametric Expressions with Python f-Strings All gradient expressions in OpenVCAD are **strings** that get compiled at runtime. To make your designs parametric, you can use Python f-strings to inject variable values into expression strings: ```python slope = 0.18 offset = 5.5 expr = f'{slope} * z + {offset}' # becomes: '0.18 * z + 5.5' ``` A few tips: - **Numeric formatting**: Use `f'{val:.4f}'` to control decimal places when precision matters. - **Curly braces**: The `{variable}` syntax is replaced by the variable's value. Everything outside braces is literal. - **Mixing variables**: You can combine Python parameters with OpenVCAD coordinate variables freely: `f'{amp} * sin({freq} * x)'`. For the full list of available math operators, functions (`sin`, `exp`, `clamp`, etc.), and coordinate variables, see [Math expressions in OpenVCAD](math-expressions.md). --- ## Coordinate Systems at a Glance Every expression can use any combination of these spatial variables: | System | Variables | Description | |---|---|---| | Cartesian | `x`, `y`, `z` | Standard axes | | Cylindrical | `rho`, `phic`, `z` | Distance from Z axis, azimuthal angle, height | | Spherical | `r`, `theta`, `phis` | Distance from origin, azimuthal angle, polar angle | | Signed Distance | `d` | Distance to the object surface (negative = inside, 0 = surface, positive = outside) | You can mix variables from different systems in a single expression. See [Math expressions in OpenVCAD](math-expressions.md) for complete definitions and all available operators. --- ## Lesson 1: Linear Gradients The simplest gradient is a linear function of a single spatial variable. The general form is: **f(x) = slope * x + offset** This maps any Cartesian axis to a linearly varying attribute value. A **power-law** gradient uses the form **(normalized_position)^n** to create a non-uniform ramp where the exponent `n` controls how quickly the value grows. In this example, both gradients are attached to the same rectangular prism along the X axis: - **Modulus** (linear): maps x in \[-20, +20\] to \[1, 10\] MPa - **Density** (power law with n=3): same range, but the value stays low for most of the span and rises sharply near the end ```python import os import pyvcad as pv import pyvcad_rendering as viz import matplotlib.pyplot as plt import numpy as np length = 40.0 width = 10.0 height = 10.0 half_len = length / 2.0 # Linear: maps x in [-20, +20] to [1, 10] mod_min = 1.0 mod_max = 10.0 slope = (mod_max - mod_min) / length offset = (mod_max + mod_min) / 2.0 modulus_expr = f'{slope}*x + {offset}' # Power law: same range, exponent n=3 n = 3.0 power_law_expr = f'({mod_min} + ({mod_max} - {mod_min}) * ((x + {half_len}) / {length}) ^ {n})' prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, width, height)) prism.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(modulus_expr)) prism.set_attribute(pv.DefaultAttributes.DENSITY, pv.FloatAttribute(power_law_expr)) root = prism viz.Render(root) ```

Plot

Linear vs Power Law

Render (modulus)

Linear Modulus

Render (density)

Power Law Density
If you want to build the same linear modulus gradient with a Python function instead of an expression string, the Numba version is still short: ```python import pyvcad as pv from numba import cfunc, types length = 40.0 half_len = length / 2.0 @cfunc(types.float64(types.float64, types.float64, types.float64, types.float64)) def modulus_gradient(x, y, z, d): return 1.0 + 9.0 * ((x + half_len) / length) prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, 10, 10)) prism.set_attribute( pv.DefaultAttributes.MODULUS, pv.FloatAttribute(modulus_gradient), ) ``` For a complete walkthrough covering all three attribute types, see the [Python functions for attributes guide](numba-attributes.md). --- ## Lesson 2: Non-Linear Gradients Non-linear gradients provide smooth transitions, bell-shaped profiles, and exponential falloffs that are common in engineering applications. This example attaches three different non-linear gradients to the same prism: | Gradient | Expression | Attribute | |---|---|---| | **Sigmoid** | `min + (max - min) / (1 + exp(-k * x))` | Modulus | | **Gaussian** | `A * exp(-(x^2) / (2 * sigma^2))` | Temperature | | **Exponential Decay** | `A * exp(-rate * (x + half_len))` | Density | The **sigmoid** creates a smooth step transition — useful for transitioning between two material regions without a hard boundary. The steepness parameter `k` controls how sharp the transition is. The **Gaussian** creates a bell-curve peak centered at the origin — useful for localized reinforcement or heating profiles. The **exponential decay** starts high and drops off — useful for modeling diffusion or attenuation from a surface. ```python import os import pyvcad as pv import pyvcad_rendering as viz import matplotlib.pyplot as plt import numpy as np length = 50.0 half_len = length / 2.0 # Sigmoid: smooth step along X k = 0.2 sig_min, sig_max = 1.0, 10.0 sigmoid_expr = f'{sig_min} + ({sig_max} - {sig_min}) / (1 + exp(-{k} * x))' # Gaussian: bell curve centered at origin amplitude, sigma = 10.0, 10.0 gaussian_expr = f'{amplitude} * exp(-(x^2) / (2 * {sigma}^2))' # Exponential decay from left edge decay_rate, exp_amplitude = 0.06, 10.0 exponential_expr = f'{exp_amplitude} * exp(-{decay_rate} * (x + {half_len}))' prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, 10, 10)) prism.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(sigmoid_expr)) prism.set_attribute(pv.DefaultAttributes.TEMPERATURE, pv.FloatAttribute(gaussian_expr)) prism.set_attribute(pv.DefaultAttributes.DENSITY, pv.FloatAttribute(exponential_expr)) root = prism viz.Render(root) ```

Plot

Non-Linear Gradients

Render (modulus — sigmoid)

Sigmoid Modulus

Render (temperature — Gaussian)

Gaussian Temperature

Render (density — exponential)

Exponential Density
--- ## Lesson 3: Periodic Gradients Periodic gradients use trigonometric functions to create oscillating patterns. They are useful for alternating material layers, wave-like stiffness profiles, and decorative color bands. This example applies two periodic gradients to a rectangular prism: - **Sinusoidal modulus**: oscillates along X with a base value and amplitude - **Color bands**: uses phase-shifted sine waves on the red and blue channels to create alternating stripes ```python import os import pyvcad as pv import pyvcad_rendering as viz import matplotlib.pyplot as plt import numpy as np length = 50.0 half_len = length / 2.0 # Sinusoidal modulus frequency = 0.5 mod_base, mod_amplitude = 5.0, 4.0 sin_expr = f'{mod_base} + {mod_amplitude} * sin({frequency} * x)' # Color bands using phase-shifted sine color_freq = 0.8 r_expr = f'0.5 * sin({color_freq} * x) + 0.5' g_expr = '0.2' b_expr = f'0.5 * sin({color_freq} * x + 3.14159) + 0.5' a_expr = '1.0' prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, 10, 10)) prism.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(sin_expr)) color_attr = pv.Vec4Attribute(r_expr, g_expr, b_expr, a_expr) prism.set_attribute(pv.DefaultAttributes.COLOR_RGBA, color_attr) root = prism viz.Render(root) ```

Plot

Periodic Gradients

Render (modulus)

Sinusoidal Modulus

Render (color)

Periodic Color Bands
--- ## Lesson 4: Multi-Axis Gradients Gradients can be functions of two or more spatial variables simultaneously. This creates 2D patterns across the surface of an object. This example uses a flat square slab and applies: - **Diagonal color gradient**: `f(x, y) = (x + y)` normalized to \[0, 1\], producing a corner-to-corner color sweep - **Radial modulus**: `sqrt(x^2 + y^2)` normalized, producing a bull's-eye pattern from center to edges - **Radial volume fractions**: two-material blend that transitions from one material at the center to another at the edges ```python import os import pyvcad as pv import pyvcad_rendering as viz import numpy as np side = 40.0 half_side = side / 2.0 max_rho = half_side * np.sqrt(2) materials = pv.default_materials # Diagonal color: (x + y) normalized diag_r = f'clamp((x + y + {side}) / ({2 * side}), 0, 1)' diag_g = f'1.0 - clamp((x + y + {side}) / ({2 * side}), 0, 1)' # Radial modulus from center radial_expr = f'clamp(sqrt(x^2 + y^2) / {max_rho}, 0, 1)' # Volume fractions: radial blend vf_a = f'clamp(sqrt(x^2 + y^2) / {half_side}, 0, 1)' vf_b = f'1.0 - clamp(sqrt(x^2 + y^2) / {half_side}, 0, 1)' slab = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(side, side, 10)) slab.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(diag_r, diag_g, '0.3', '1.0')) slab.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(radial_expr)) slab.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_a, materials.id("red")), (vf_b, materials.id("blue")) ])) root = slab viz.Render(root, materials) ```

Plot (2D heatmaps)

Multi-Axis Gradients

Render (color — diagonal)

Diagonal Color

Render (modulus — radial)

Radial Modulus

Render (volume fractions — radial)

Radial Volume Fractions
--- ## Lesson 5: Signed-Distance Gradients The special variable `d` represents the **signed distance** to the object's implicit surface: - **d < 0**: inside the object - **d = 0**: on the surface - **d > 0**: outside the object (not sampled during rendering) This makes `d` ideal for creating **surface-following** gradients — stiff skins, colored shells, or material transitions that conform to the object's shape regardless of its geometry. This example applies three `d`-based gradients to a sphere: - **Skin modulus**: exponential decay from surface inward — high stiffness at the shell, soft in the core - **Shell color**: red on the surface transitioning to blue in the interior - **Volume fractions**: stiff material at the skin, soft material in the core > **Note:** The renders below show a clipped cross-section to reveal the internal gradient. ```python import os import pyvcad as pv import pyvcad_rendering as viz radius = 15.0 materials = pv.default_materials # Skin modulus: high at surface, decays inward skin_thickness = 3.0 skin_min, skin_max = 1.0, 10.0 skin_expr = f'{skin_min} + ({skin_max} - {skin_min}) * exp(d / {skin_thickness})' # Shell color: red at surface, blue in core shell_t = 2.0 shell_r = f'clamp(1.0 + d / {shell_t}, 0, 1)' shell_b = f'clamp(-d / {shell_t}, 0, 1)' # Volume fractions: skin vs core vf_skin = f'clamp(1.0 + d / {skin_thickness}, 0, 1)' vf_core = f'1.0 - clamp(1.0 + d / {skin_thickness}, 0, 1)' sphere = pv.Sphere(pv.Vec3(0, 0, 0), radius) sphere.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(skin_expr)) sphere.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(shell_r, '0.1', shell_b, '1.0')) sphere.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_skin, materials.id("red")), (vf_core, materials.id("blue")) ])) root = sphere viz.Render(root, materials) ```

Plot

Signed Distance Gradients

Render (modulus — clipped)

Skin Modulus

Render (color — clipped)

Shell Color

Render (volume fractions — clipped)

Skin Volume Fractions
--- ## Lesson 6: Cylindrical Coordinate Gradients Cylindrical coordinates are natural for objects with rotational symmetry about the Z axis. The key variables are: - `rho` — radial distance from the Z axis - `phic` — azimuthal angle (radians, -pi to +pi) - `z` — height (same as Cartesian z) This example applies gradients to a cylinder: - **Radial modulus**: stiffness increases linearly from center (`rho=0`) to edge (`rho=R`) - **Angular color**: a color sweep around the circumference using `phic` - **Radial volume fractions**: two-material blend from core to edge > **Note:** The modulus and volume fraction renders below show a clipped cross-section to reveal the internal radial gradient. ```python import os import pyvcad as pv import pyvcad_rendering as viz cyl_radius = 15.0 cyl_height = 30.0 materials = pv.default_materials # Radial modulus mod_min, mod_max = 1.0, 10.0 radial_expr = f'{mod_min} + ({mod_max} - {mod_min}) * (rho / {cyl_radius})' # Angular color sweep r_expr = f'clamp((phic + 3.14159) / (2 * 3.14159), 0, 1)' g_expr = f'clamp(1.0 - (phic + 3.14159) / (2 * 3.14159), 0, 1)' # Volume fractions: radial blend vf_edge = f'clamp(rho / {cyl_radius}, 0, 1)' vf_core = f'1.0 - clamp(rho / {cyl_radius}, 0, 1)' cylinder = pv.Cylinder(pv.Vec3(0, 0, 0), cyl_radius, cyl_height) cylinder.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(radial_expr)) cylinder.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(r_expr, g_expr, '0.3', '1.0')) cylinder.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_edge, materials.id("red")), (vf_core, materials.id("blue")) ])) root = cylinder viz.Render(root, materials) ```

Plot

Cylindrical Gradients

Render (modulus — clipped)

Radial Modulus

Render (color — angular)

Angular Color

Render (volume fractions — clipped)

Radial Volume Fractions
--- ## Lesson 7: Spherical Coordinate Gradients Spherical coordinates are ideal for objects with radial symmetry from a central point. The key variables are: - `r` — radial distance from the origin - `theta` — azimuthal angle in the XY plane (radians, -pi to +pi) - `phis` — polar angle from the +Z axis (radians, 0 to pi) This example applies gradients to a sphere: - **Radial modulus**: stiffness increases from center (`r=0`) to surface (`r=R`) - **Polar color**: color varies from the north pole (`phis=0`) to the south pole (`phis=pi`) - **Radial volume fractions**: two-material blend from center to surface > **Note:** The modulus and volume fraction renders below show a clipped cross-section to reveal the internal radial gradient. ```python import os import pyvcad as pv import pyvcad_rendering as viz sph_radius = 15.0 materials = pv.default_materials # Radial modulus mod_min, mod_max = 1.0, 10.0 radial_expr = f'{mod_min} + ({mod_max} - {mod_min}) * (r / {sph_radius})' # Polar angle color: north=blue, south=red r_expr = f'clamp(phis / 3.14159, 0, 1)' b_expr = f'clamp(1.0 - phis / 3.14159, 0, 1)' # Volume fractions: center vs surface vf_outer = f'clamp(r / {sph_radius}, 0, 1)' vf_inner = f'1.0 - clamp(r / {sph_radius}, 0, 1)' sphere = pv.Sphere(pv.Vec3(0, 0, 0), sph_radius) sphere.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(radial_expr)) sphere.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(r_expr, '0.2', b_expr, '1.0')) sphere.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_outer, materials.id("red")), (vf_inner, materials.id("blue")) ])) root = sphere viz.Render(root, materials) ```

Plot

Spherical Gradients

Render (modulus — clipped)

Radial Modulus

Render (color — polar)

Polar Color

Render (volume fractions — clipped)

Radial Volume Fractions
--- ## Summary: Gradient Toolbox | Gradient Type | Expression Pattern | Best For | |---|---|---| | **Linear** | `m * x + b` | Uniform property ramps | | **Power Law** | `((x - min) / range) ^ n` | Tunable non-uniform ramps | | **Sigmoid** | `1 / (1 + exp(-k * x))` | Smooth step transitions | | **Gaussian** | `exp(-(x^2) / (2*s^2))` | Localized peaks | | **Exponential** | `exp(-rate * x)` | Decay / attenuation | | **Sinusoidal** | `sin(freq * x)` | Periodic layers / bands | | **Multi-axis** | `f(x, y)` | 2D patterns, diagonal sweeps | | **Signed distance** | `f(d)` | Surface-following skins | | **Cylindrical** | `f(rho)`, `f(phic)` | Radial and angular patterns | | **Spherical** | `f(r)`, `f(phis)` | Radial and polar patterns | For the complete list of available operators, functions, and coordinate variables, see [Math expressions in OpenVCAD](math-expressions.md).