Metadata-Version: 2.4
Name: vcti-shader-derive
Version: 1.0.0
Summary: The derived-value shader feature: the fragment-stage math that reduces a multi-component result to one scalar.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-derive
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-derive/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: vcti-shader-base>=1.0.0
Requires-Dist: vcti-derived>=2.0.1
Provides-Extra: gl
Requires-Dist: vcti-shader-compiler[gl]>=3.0.0; extra == "gl"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: vcti-shader-compiler>=3.0.0; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-shader-derive

The derived-value shader feature: the fragment-stage math that reduces a multi-component result to one scalar.

## Overview

A CAE solver reports vectors and tensors — a displacement per node, a stress
tensor per element. A colormap needs a single number. Turning one into the other
is what `vcti-shader-derive` does, on the GPU, at draw time: given a stress
tensor it produces von Mises, or a principal, or the `xy` component, and the
`fringe` feature colours whatever comes out.

Doing it in the shader rather than ahead of it is the point. The result buffer
is uploaded once, and switching from von Mises to max principal, or from one
component order to another, sets a uniform — no re-upload, no CPU pass, no
rebuilt shader.

The package ships one Slang module per data family:

| Family | Components | What it derives |
|---|---|---|
| `scalar` | 1 | identity, absolute, unit conversions |
| `vector2`, `vector3` | 2, 3 | components, magnitude |
| `dof6_2d`, `dof6_3d` | 4, 6 | translational and rotational components and magnitudes |
| `symtensor2d` | 3 | components, mean, von Mises, principals, max shear |
| `symtensor3d` | 6 | the above plus invariants, equivalent strain, intensity, deviatoric principals |

Around that math the package declares two things: the **specs** saying what each
family needs supplied — which attributes, which uniforms — and the
**`ShaderDefinition`** saying what the feature is. Both are plain data. Nothing
here compiles or runs a shader; a build step does that, using what this package
declares.

The math also exists on the CPU, in [`vcti-derived`](https://github.com/vcollab/vcti-python-derived),
which is where value ranges, legends and exports get the same numbers. Two
implementations can disagree, so the test suite runs every dispatch case of every
family headlessly on a real GPU — through the compiler's `render_readback` — and
diffs it against the catalog. That is test machinery, and it ships with the
source, not with the package.

## Installation

```bash
pip install vcti-shader-derive
```

Requires Python 3.12, 3.13, or 3.14, matching `vcti-shader-base` and
`vcti-derived`. Nothing native is built on that path, and nothing native is built
by `[test]` either — only the `[gl]` extra pulls a GL binding, and only on 3.14
does that compile from source for want of a cp314 wheel.

### In `requirements.txt`

```
vcti-shader-derive>=1.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-shader-derive>=1.0.0",
]
```

---

## Quick Start

### What the feature is

```python
from vcti.shader.derive import DEFINITION

DEFINITION.id, DEFINITION.role.value      # ('derive', 'fragment')
DEFINITION.capabilities                   # ('scalar', 'vector2', 'vector3', 'dof6_2d', …)
len(DEFINITION.slang_modules)             # 7 — one per family
```

`DEFINITION` is built at import, so importing the package is all it takes to
have one. Its `capabilities` are one tag per data family, not one for the
feature: a shader over a stress tensor takes different inputs from one over a
displacement vector, and the two are not interchangeable, so a build step selects
on the family it has data for.

### Which derived types a family offers

Every derived type a family can produce is a case of one `u_derivedType`
integer, and the table is built from the `vcti-derived` catalog rather than
listed here:

```python
from vcti.derived import DataFamily
from vcti.shader.derive import build_dispatch_table

cases = build_dispatch_table()[DataFamily.SYMTENSOR3D]
[(c.value, c.name) for c in cases][:4]
# [(0, 'xx'), (1, 'yy'), (2, 'zz'), (3, 'xy')]

next(c for c in cases if c.name == "von_mises").value    # 7 — write this to u_derivedType
```

Not everything in the catalog is here. A derived type ships as a GPU case only
if it produces a scalar and all of its parameters have defaults — so principal
*directions* and traction vectors are absent, because there is nothing to colour
and a plane normal is field data rather than a uniform.

**These integers are a wire contract.** They are compiled into the `switch` in
each `.slang`, written into built shaders, and stored by whatever selected one,
so they are append-only; `tests/test_dispatch.py` pins the whole table so a
catalog reordering fails loudly instead of silently renumbering what has already
shipped.

### What its math needs supplied

Pick a family, and the specs follow from it:

```python
from vcti.shader.derive import (
    component_order_uniform, derived_type_uniform, parameter_uniforms, result_attributes,
)

[(a.name, a.type, a.semantic) for a in result_attributes(DataFamily.SYMTENSOR3D)]
# [('a_result0', 'vec3', 'result.0'), ('a_result1', 'vec3', 'result.1')]

component_order_uniform(DataFamily.SYMTENSOR3D).gl_type          # 'int[6]'
derived_type_uniform({c.name: c.value for c in cases}).name      # 'u_derivedType'
[u.name for u in parameter_uniforms(p for c in cases for p in c.parameters)]
# ['u_nu'] — equivalent strain's Poisson ratio, the family's one parameter
```

Three things are worth knowing about that.

**Results arrive packed.** Multi-component data goes into `vec3` attributes —
`ceil(n / 3)` of them, zero-padded — because a vertex attribute is at most four
components wide and a six-component tensor does not fit in one. The `scalar`
family is the exception and uses a single `float`.

**Component order is a uniform, not a repack.** `u_componentOrder` maps each
canonical slot to the lane the host actually packed it into, mirroring
`vcti-derived`'s `ComponentOrder`. A solver that writes `(xx, xy, yy, …)` is
drawn by setting six integers rather than by rewriting the buffer.

**Parameters are uniforms too.** A defaultable parameter — equivalent strain's
`nu`, an affine conversion's `scale` and `offset` — becomes `u_<name>`, so the
derived type stays one dispatch case instead of one shader per value.

### Building a shader from it

This package does not compile anything, so the last step belongs to a build
step:

```python
# 1. Give the compiler somewhere to resolve `import derive_symtensor3d;` from.
include_dirs = [DEFINITION.slang_dir]

# 2. Merge this feature's specs with a vertex-stage feature's.
attributes = vertex_attributes(stage) + result_attributes(family)
```

`slang_dir` is the field with teeth: the `.slang` files install inside this
package, so only this package can resolve where they are.

---

## Dependencies

- [`vcti-shader-base`](https://github.com/vcollab/vcti-python-shader-base) —
  the zero-dependency vocabulary this feature declares itself with
  (`ShaderDefinition`, `StageRole`, `AttributeSpec`, `UniformSpec`).
- [`vcti-derived`](https://github.com/vcollab/vcti-python-derived) — the catalog
  of derived fields. Unlike its sibling features, derive cannot describe itself
  without a second dependency: the dispatch table, the capability tags and the
  contract builders are all read off the catalog at import. The feature is a
  mirror of that catalog's GPU-supported subset, and the dependency is what keeps
  the mirror from drifting.

The rest are **test-only** — needed to run the shader, not to declare the
feature, and split across two extras:

- `[test]` — [`vcti-shader-compiler>=3.0`](https://github.com/vcollab/vcti-python-shader-compiler),
  whose `render_readback` the suite drives to run the emitted math headlessly.
  The test module imports the compiler at module scope, so without it the tests
  fail to collect rather than skip — which is why it sits here and not under
  `[gl]`.
- `[gl]` — `vcti-shader-compiler[gl]`, the GL binding that makes the probes
  actually run. Every shader package defines a `gl` extra meaning "what I need
  to execute shaders"; `ci-shader.yml` installs `.[test,gl]` on that contract.

Without `[gl]` the probe tests skip and everything else passes, which is the
right default: compiling a GL binding is a cost only the machines that run
shaders should pay.

Nothing that combines, selects, or discovers features: a feature declares itself
and stops there.

---

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| Understand the derived-value model and the design decisions | [docs/design.md](docs/design.md) |
| Navigate or modify the source, including the Slang | [docs/source-guide.md](docs/source-guide.md) |

The full API reference is generated from the source docstrings and published in
the unified VCollab docs.
