# Material API Reference

## Core Classes

### Material

A dataclass representing a material with fully dynamic property tracking.
All properties - constants and dependency-driven expressions - are assigned
dynamically and tracked automatically via a `properties` dict. No material
type, composition, or fixed schema required.

```python
from materforge.core.materials import Material
```

#### Methods

##### `property_names() -> set`

Returns all dynamically assigned property names.

```python
props = material.property_names()
# {'density', 'heat_capacity', 'solidus_temperature', ...}
```

##### `evaluate(symbol, value) -> Material`

Evaluates all properties by substituting `symbol = value`. Returns a **new
`Material`** instance with all expressions reduced to numeric SymPy values.
Properties that still have free symbols after substitution, or that fail
evaluation, are excluded and logged as errors.

```python
def evaluate(self, symbol: sp.Symbol, value: Union[float, int]) -> Material:
```

**Parameters:**
- `symbol`: SymPy symbol to substitute - must match the symbol passed to
  `create_material()`. Must be `sp.Symbol`.
- `value`: Numeric value to substitute - must be convertible to `float`

**Returns:**
- A new `Material` named `"{name}@{symbol}={value}"` with all properties
  evaluated to numeric SymPy scalars (`sp.Float`)

**Raises:**
- `ValueError`: If `symbol` is not `sp.Symbol`
- `ValueError`: If `value` is `None` or not convertible to `float`

**Example:**
```python
import sympy as sp
from materforge.parsing.api import create_material

T = sp.Symbol('T')
mat = create_material('myAlloy.yaml', dependency=T)

# Evaluate all properties at T = 500 K - returns a new Material
mat_at_500 = mat.evaluate(T, 500.0)
print(mat_at_500.heat_capacity)           # sp.Float numeric value
print(float(mat_at_500.heat_capacity))    # Python float if needed

# Access symbolic expressions directly via dot notation on the original
print(mat.density)            # sp.Float(7000.0) for a constant
print(mat.heat_conductivity)  # SymPy Piecewise expression in T
```

> **pystencils users:** Do not pass `u.center()` to `create_material()`.
> Always create the material with a plain `sp.Symbol`, then substitute
> to the field accessor at the pystencils boundary:
> ```python
> T = sp.Symbol('T')
> mat = create_material('myAlloy.yaml', dependency=T)
> expr = mat.thermal_diffusivity.subs(T, u.center())   # one explicit coupling point
> ```

#### Repr

```python
str(mat)   # Material: myAlloy (10 properties)
repr(mat)  # Material(name='myAlloy', properties=['density', 'heat_capacity', ...])
```

---

## Main API Functions

### `create_material`

Creates a `Material` instance from a YAML configuration file.

```python
from materforge.parsing.api import create_material

def create_material(
    yaml_path: Union[str, Path],
    dependency: sp.Symbol,
    enable_plotting: bool = True,
) -> Material:
```

**Parameters:**
- `yaml_path`: Path to the YAML configuration file
- `dependency`: SymPy symbol used as the independent variable in all property
  expressions. Must be a plain `sp.Symbol`. pystencils field accessors such
  as `u.center()` are not valid here - apply `.subs()` after creation instead.
- `enable_plotting`: Whether to generate and save property plots (default: `True`)

**Returns:**
- `Material`: Fully initialised material with all properties assigned

**Raises:**
- `FileNotFoundError`: If the YAML file does not exist
- `TypeError`: If `dependency` is not a `sp.Symbol` instance
- `MaterialConfigError`: If the YAML content is invalid or property processing fails
- `PropertyConfigError`: If a specific property block is structurally invalid

**Example:**
```python
import sympy as sp
from materforge.parsing.api import create_material

T = sp.Symbol('T')
mat = create_material('myAlloy.yaml', dependency=T)
mat_no_plots = create_material('myAlloy.yaml', dependency=T, enable_plotting=False)
```

---

### `validate_yaml_file`

Validates a YAML file structure without creating the material.

```python
from materforge.parsing.api import validate_yaml_file

def validate_yaml_file(yaml_path: Union[str, Path]) -> bool:
```

**Parameters:**
- `yaml_path`: Path to the YAML configuration file

**Returns:**
- `bool`: `True` if the file is structurally valid

**Raises:**
- `FileNotFoundError`: If the file does not exist
- `MaterialConfigError`: If the top-level YAML structure is invalid
- `PropertyConfigError`: If a specific property block is structurally invalid

**Example:**
```python
from materforge.parsing.api import validate_yaml_file
from materforge.parsing.validation.errors import MaterialConfigError, PropertyConfigError

try:
    is_valid = validate_yaml_file('myAlloy.yaml')
    print(f"Valid: {is_valid}")
except PropertyConfigError as e:
    print(f"Property config error: {e}")
except MaterialConfigError as e:
    print(f"Config error: {e}")
```

---

### `get_material_info`

Returns metadata about a YAML file without fully processing the material.

```python
from materforge.parsing.api import get_material_info

info = get_material_info('myAlloy.yaml')
print(info['name'])              # 'myAlloy'
print(info['total_properties'])  # 10
print(info['properties'])        # ['density', 'heat_capacity', ...]
print(info['property_types'])    # {'CONSTANT_VALUE': 5, 'TABULAR_DATA': 1, ...}
```

---

### `get_material_property_names`

Returns all property names on an already-created material. Equivalent to
`mat.property_names()` - prefer calling `mat.property_names()` directly.

```python
from materforge.parsing.api import get_material_property_names

names = get_material_property_names(mat)   # returns a set
```

**Raises:**
- `TypeError`: If `material` is not a `Material` instance

---

### `evaluate_material_properties`

Functional wrapper around `Material.evaluate()`. Equivalent to
`mat.evaluate(symbol, value)` - prefer calling `mat.evaluate()` directly.

```python
from materforge.parsing.api import evaluate_material_properties

mat_at_500 = evaluate_material_properties(mat, T, 500.0)
```

**Raises:**
- `TypeError`: If `material` is not a `Material` instance

---

## YAML Placeholder Contract

All YAML equation strings **must** use `T` as the dependency variable.
`T` is the fixed placeholder (`YAML_PLACEHOLDER = sp.Symbol('T')`),
hardcoded in materforge. At runtime, `T` is substituted with whatever
`sp.Symbol` the caller passes to `create_material()`.

```yaml
# Correct - always use T in YAML equations
specific_enthalpy:
  dependency: (1773, 293, 541)
  equation: Integral(heat_capacity, T)
  bounds: [constant, constant]

thermal_diffusivity:
  dependency: (1773, 293, 100)
  equation: thermal_conductivity / (density * specific_heat)
  bounds: [constant, constant]
```

```yaml
# Wrong - any symbol other than T in the equation will cause an error
specific_enthalpy:
  equation: Integral(heat_capacity, u_C)   # crashes unconditionally
```

When YAML uses any symbol other than `T`, the exact exception depends on
property type:

- **`COMPUTED_PROPERTY`**: raises `DependencyError` - `sp.sympify()` parses
  the non-`T` symbol as a plain `sp.Symbol`. The dependency filter
  (`if s != YAML_PLACEHOLDER`) does not exclude it, so it lands in the
  dependency list. Since `u_C` (or any other name) is not a material property,
  materforge raises:
  ```
  DependencyError: Missing dependencies ['u_C'] in expression
      'Integral(heat_capacity, u_C)': u_C
  Available properties: density, heat_capacity, ...
  Please check for typos or add the missing properties to your configuration.
  ```

- **`PIECEWISE_EQUATION`**: raises `ValueError` - equations in this type are
  validated upfront and may only reference `T`. Any other symbol is rejected
  immediately:
  ```
  ValueError: Unexpected symbol(s) [u_C] in equation '7877.39-0.37*u_C'
      for property 'viscosity'. PIECEWISE_EQUATION equations must use 'T'
      as the only variable.
  ```
  To reference other material properties in an expression, use
  `COMPUTED_PROPERTY` instead.

Both errors occur regardless of what symbol the caller passes to
`create_material()`. The YAML and the Python caller are fully decoupled -
the YAML always uses `T`, and the caller can use any `sp.Symbol`.

---

## Property Type Enum

```python
from materforge.parsing.validation.property_type_detector import PropertyType

PropertyType.CONSTANT_VALUE      # single numeric value
PropertyType.STEP_FUNCTION       # discontinuous transition at a scalar reference
PropertyType.FILE_IMPORT         # data loaded from .csv / .xlsx / .txt
PropertyType.TABULAR_DATA        # explicit dependency-value pairs
PropertyType.PIECEWISE_EQUATION  # symbolic equations over dependency ranges
PropertyType.COMPUTED_PROPERTY   # derived from other properties via equation
```

Note: `PIECEWISE_EQUATION` equations may only reference `T` (the YAML
placeholder). To combine multiple material properties in an expression, use
`COMPUTED_PROPERTY` instead.

---

## Error Handling

MaterForge raises standard Python exceptions with descriptive messages that
include the property name and config path where the failure occurred.

| Situation | Exception |
|---|---|
| YAML file not found | `FileNotFoundError` |
| Invalid YAML syntax | `ruamel.yaml.scanner.ScannerError` |
| Duplicate key in YAML | `ruamel.yaml.constructor.DuplicateKeyError` |
| Unknown top-level YAML key | `MaterialConfigError` |
| Missing `name` or `properties` block | `MaterialConfigError` |
| `properties` block is empty | `MaterialConfigError` |
| `dependency` not `sp.Symbol` in `create_material()` | `TypeError` |
| `material` wrong type in API helpers | `TypeError` |
| `symbol` not `sp.Symbol` in `evaluate()` | `ValueError` |
| `value` non-numeric in `evaluate()` | `ValueError` |
| Invalid property configuration | `PropertyConfigError` |
| Non-`T` symbol in `PIECEWISE_EQUATION` | `ValueError` |
| Non-`T` symbol in `COMPUTED_PROPERTY` | `DependencyError` |
| Missing property dependency | `DependencyError` |
| Circular property dependency | `CircularDependencyError` |
| File import column not found | `ValueError` |

`MaterialConfigError`, `PropertyConfigError`, `DependencyError`, and
`CircularDependencyError` are importable from
`materforge.parsing.validation.errors`.
