# waterfall

Quick CFD solver that trades Navier-Stokes integration for a ray-based pressure and
surface-normal model. The goal is practical, high-resolution flow insight without the
overhead of full CFD, while still producing usable drag and downforce coefficients.

## What it does
- Loads an STL mesh and slices it along the Z axis.
- Samples surface normals on a regular grid per slice and estimates curvature relative
  to the flow direction.
- Propagates a persistent ray field through slices to derive pressure, velocity, and
  drag metrics aligned with the incoming flow.
- Writes text matrices and heatmap images per slice, plus a JSON summary.

## Configuration
Edit `config.json` before running:
- `stl_path`: Path to the STL file.
- `slice_depth`: Thickness of each Z slice.
- `r1_resolution`, `r2_resolution`: Ray sampling resolution per slice where
  `r1_resolution` is radial rays per sector and `r2_resolution` is the number of
  angular sectors (must be at least 2). Output matrices remain `r1 x r2` in
  polar order (radius, sector).
- `output_path`: Output directory for results.
- `flow_direction` (optional): 3-element vector for incoming flow direction.
  Defaults to `[0, 0, 1]`.
- `ray_weight` (optional): Scalar weight per ray cell (defaults to `1.0`).
- `numba_parallel` (optional): Enable multi-core ray processing via Numba
  (defaults to `true` when Numba is installed).
- `airspeed`: Flow speed in meters per second.
- `air_density`: Air density in kg/m^3.
- `units`: Label for your geometry units (`m`, `cm`, `mm`, `in`, or `ft`).
- `model_label` (optional): Friendly name stored in the calibration log.
- `calibration_path` (optional): Path to a calibration JSON file. Defaults to
  `<output_path>/calibration.json`.

Example:
```json
{
  "stl_path": "model.STL",
  "units": "mm",
  "slice_depth": 50,
  "r1_resolution": 30,
  "r2_resolution": 30,
  "airspeed": 30,
  "air_density": 1.225,
  "output_path": "./output",
  "flow_direction": [0, 0, 1],
  "ray_weight": 1.0
}
```

## Usage
Install dependencies (Python 3.10+ recommended):
```bash
pip install numpy trimesh matplotlib rtree scipy
```

Optional multi-core acceleration:
```bash
pip install numba
```

Run:
```bash
python main.py
```

## Outputs
For each slice `N`, the script writes:
- `slice_N_pressure.txt`
- `slice_N_velocity.txt`
- `slice_N_drag.txt`
- `slice_N_pressure.png`
- `slice_N_velocity.png`
- `slice_N_drag.png`

It also writes `summary.txt` containing a JSON payload with metadata such as the total
drag metric, force values, slice count, resolutions, STL path, and flow direction.
Each run also appends a record to `calibration.json` with computed coefficients and
placeholders for real-world values.

Console output includes drag, lift, and downforce along with calibrated values when a
calibration file is present. Forces are reported in Newtons using the
dynamic pressure (`0.5 * air_density * airspeed^2`) times each cell area, with optional
`ray_weight` scaling. Drag/lift coefficients are reported as:
`C = Force / (dynamic_pressure * reference_area)`.

### Calibration workflow
Each run appends an entry to `calibration.json` with the computed drag coefficient and
downforce. Fill in the corresponding real-world values for known models, then rerun to
automatically scale drag/downforce using the averages of the known ratios.

Example calibration entry:
```json
{
  "label": "baseline-model",
  "stl_path": "model.STL",
  "computed_drag_coefficient": 0.42,
  "computed_downforce": 860.0,
  "expected_drag_coefficient": 0.31,
  "expected_downforce": 800.0
}
```

The script also emits a calibrated drag force/coefficient estimate that scales the
pressure-based drag proxy by the ratio of the heuristic drag metric to the total
pressure metric, so you can compare the heuristic output with a more physical, area-
and dynamic-pressure-scaled estimate.

## Notes / limitations
- This solver focuses on practicality over full Navier-Stokes accuracy.
- Calibration improves absolute drag/downforce agreement when you supply real-world
  reference data.
- Ensure the STL is valid and non-empty. Invalid meshes can yield undefined results.
