# Simulation Models Guide

This guide covers the implemented photovoltaic simulation approaches: the real-sky batch model (REST API), the fast horizon-based simulation branch (REST API), and co-simulation (CLI script).

## Contents

- [Which approach?](#which-approach)
- [Real-sky batch simulation](#real-sky-batch-simulation)
- [Fast Horizon-Based PV Simulation](#fast-horizon-based-pv-simulation)
- [Co-simulation (CLI)](#co-simulation-cli)
- [Common problems](#common-problems)

---

## Which Approach?

| | Co-Sim | Real-Sky Batch | Fast Horizon-Based |
|---|---|---|---|
| **How to run** | CLI script (`run_cosim.py`) | REST API (`POST /solar/real-sky-batch`) | REST API (`POST /solar/fast-horizon-pv-simulation`) |
| **Timestamps** | One at a time | Many in a single request | Many in a single request |
| **Output** | JSON printed to terminal | Partitioned Parquet files on disk | One Parquet timeseries file per suitable area |
| **Best for** | Quick spot-checks and development | Full real-sky runs using clear-sky area means | Faster per-area runs driven by merged centroid horizon profiles |

Use **co-sim** when you want a fast single-timestamp check from the command line.

Use **real-sky batch** when you need many rooftop areas, many timestamps, and the existing clear-sky area-means based simulation path.

Use **fast horizon-based simulation** when you want a lighter per-area branch that reuses horizon-analysis outputs directly and writes one timeseries parquet per suitable area.

The three approaches do not use exactly the same prerequisites:

- `suitable_areas.parquet`
- `area_clearsky_means` outputs for real-sky batch and co-simulation
- existing horizon-analysis outputs for the fast horizon-based branch
- weather input or weather artifact, depending on the branch

---

## Real-Sky Batch Simulation

### Batch Workflow Map

The implemented simulation branches consume different prepared inputs and write different result layouts.

```mermaid
flowchart LR
    A[Suitable areas parquet] --> D[Real-sky batch]
    B[Area means parquet] --> D
    C[Weather file] --> D
    D --> E[Partitioned Parquet results]
    A --> F[Fast horizon PV]
    G[Horizon rasters] --> F
    H[Uploaded weather or weather artifact] --> F
    F --> I[Per-area Parquet timeseries]
    A --> J[Co-simulation CLI]
    B --> J
```

*Figure 1: Implemented photovoltaic simulation branches, showing the distinct prerequisites and output styles of real-sky batch, fast horizon-based simulation, and co-simulation.*

### Endpoint

Use:

```text
POST /solar/real-sky-batch
```

This endpoint runs photovoltaic simulation for many rooftop areas in one request and stores partitioned output datasets.

### Required Workflow Order

Before running batch simulation, the same session should already have:

- suitable areas
- area clear-sky means
- valid weather input

In practice, the usual prerequisite chain is:

1. terrain and solar radiation pipeline
2. parquet artifact pipeline
3. weather preparation
4. real-sky batch

### Required Inputs

#### Weather Input

- `weather_file`
- `weather_columns_json`

The `weather_file` must be uploaded as part of the request.

#### Time Handling

- optional `timestamp_format`
- `timezone`

#### Parallel Configuration

- optional `ray_num_cpus`

#### PV Sizing

- `power_density_W_per_m2`

#### Temperature Model

- `temp_model`
- optional `temp_class`

#### Output Model

- `output_mode`
- `dc_ac_ratio`
- `eta_inv_nom`
- `eta_inv_ref`

### Parameter Notes

- `weather_columns_json` is a JSON mapping that tells the backend how the uploaded weather file columns map to the canonical simulation names. The required logical keys are `dni`, `dhi`, `temp_air`, `wind_speed`, and `time_col_real`.
- `timestamp_format` is optional. If it is omitted, the backend tries to auto-detect the weather timestamp format.
- `timezone` is the IANA timezone name used when the uploaded weather timestamps are naive rather than timezone-aware.
- `ray_num_cpus` is accepted by the API as a parallel-configuration parameter, but in the current batch core implementation the shared in-process simulator path is used and this value is effectively ignored.
- `power_density_W_per_m2` defines the installed DC capacity density used to size the PV system from `effective_area_m2`. It must be greater than `0`.
- `temp_model` must be either `sapm` or `pvsyst`.
- `temp_class` depends on `temp_model`. For `sapm`, the allowed values are:
  `glass_glass__open_rack`, `glass_glass__close_mount`, `glass_polymer__open_rack`, `glass_polymer__insulated_back`.
- For `pvsyst`, the allowed `temp_class` values are:
  `freestanding`, `insulated`, `semi_integrated`.
- `output_mode` must be either `dc` or `dc_ac`.
- If `output_mode = dc_ac`, then inverter parameters are validated and applied:
  `dc_ac_ratio > 0`, `0 < eta_inv_nom <= 1`, and `0 < eta_inv_ref <= 1`.

### Typical Defaults

- `power_density_W_per_m2 = 180.0`
- `temp_model = sapm`
- `output_mode = dc_ac`
- `dc_ac_ratio = 1.2`
- `eta_inv_nom = 0.96`
- `eta_inv_ref = 0.9637`

### Temperature Models

Supported values:

- `sapm` — Sandia-style temperature model
- `pvsyst` — PVSyst heat-loss style model

### Output Modes

Supported values:

- `dc`
- `dc_ac`

If you use `dc_ac`, inverter-related parameters are also applied.

### Produced Outputs

The service writes partitioned output under the canonical host-side result root:

```text
FILE_STORAGE_ROOT/real_sky_results/<location>/<mapset>/
```

The result dataset is partitioned by `timestamp_clear`, with a layout of the form:

```text
FILE_STORAGE_ROOT/real_sky_results/<location>/<mapset>/
  timestamp_clear-<value>/
    part-000000.parquet
    part-000001.parquet
    ...
```

This means each clear-sky timestamp partition can contain multiple parquet part files.

---

## Fast Horizon-Based PV Simulation

### Conceptual Role

The fast horizon-based branch is an alternative REST simulation path that avoids the downstream clear-sky area-means chain used by `real-sky-batch`.

Instead, it:

- reuses existing horizon-analysis outputs for the current session,
- samples the local horizon rasters at each suitable-area centroid,
- fetches an external horizon profile from PVGIS for the same centroid,
- converts local GRASS horizon directions to the pvlib/PVGIS azimuth convention,
- interpolates the external profile to the converted local azimuth grid,
- merges local and external horizon elevations by azimuth,
- runs a pvlib-based per-area simulation using the merged horizon profile and the resolved weather time series.

### Endpoint

Use:

```text
POST /solar/fast-horizon-pv-simulation
```

### Required Workflow Order

Before running the fast horizon-based branch, the same session should already have:

- suitable areas
- horizon-analysis outputs
- valid weather input, either uploaded at request time or already resolvable from workflow context

In practice, the usual prerequisite chain is:

1. terrain and solar radiation pipeline up to horizon analysis
2. suitable-areas generation or upload
3. weather preparation or weather upload
4. fast horizon-based simulation

### Input Modes

#### Suitable Areas

The endpoint runs on the full suitable-area dataset. It supports:

- uploaded suitable-area polygons via `suitable_areas_file`
- existing suitable-area artifact resolution when no upload is provided

Uploaded suitable-area files may already be enriched or may be auto-enriched from existing workflow context when required fields are missing.

#### Weather

The endpoint supports two weather paths:

- upload `weather_file` with `weather_columns_json`
- omit `weather_file` and resolve an existing weather dataset from workflow context

`timestamp_format` remains optional for uploaded weather files.

#### PV Parameters

The fast branch reuses the same public PV parameter style as the batch branch:

- `power_density_W_per_m2`
- `temp_model`
- optional `temp_class`
- `output_mode`
- `dc_ac_ratio`
- `eta_inv_nom`
- `eta_inv_ref`

### High-Level Logic

At a high level, the implemented service does the following for each suitable area:

1. resolve the polygon and compute its centroid
2. sample the existing local horizon rasters at that centroid
3. fetch the PVGIS horizon profile for the same centroid
4. convert local GRASS `r.horizon` directions to pvlib/PVGIS azimuths
5. interpolate PVGIS horizon elevations to the converted local azimuth grid
6. merge local and external horizon elevations using the maximum value per azimuth
7. compute solar position and apply horizon-aware direct-irradiance blocking
8. recompute plane-of-array irradiance and PV output with pvlib
9. write one Parquet timeseries file for that suitable area

### Azimuth Convention Handling

GRASS `r.horizon` and pvlib/PVGIS use different azimuth conventions. The `r.horizon` raster suffixes are stored in the GRASS directional convention: `0°` is East, `90°` is North, `180°` is West, and `270°` is South, with angles increasing counterclockwise from East. PVGIS and pvlib use azimuths clockwise from North: `0°` is North, `90°` is East, `180°` is South, and `270°` is West.

Before local DSM-derived horizon profiles are merged with PVGIS horizons or queried using pvlib solar azimuths, the fast horizon PV workflow converts the local directions:

```text
pvlib_azimuth = (90 - grass_angle) % 360
```

Examples:

- GRASS `0°` East becomes pvlib `90°` East.
- GRASS `90°` North becomes pvlib `0°` North.
- GRASS `180°` West becomes pvlib `270°` West.
- GRASS `270°` South becomes pvlib `180°` South.

After this conversion, the merged horizon profile is expressed in the pvlib/PVGIS azimuth convention. The per-timestep shading step can therefore compare pvlib solar azimuth directly against the merged horizon profile. The same conversion is applied to GRASS-derived `aspect_mean` before it is passed to pvlib as `surface_azimuth`.

### Produced Outputs

The service writes one final timeseries parquet per suitable area under:

```text
FILE_STORAGE_ROOT/fast_horizon_pv_results/<location>/<mapset>/
```

with filenames of the form:

```text
suitable_area_id-<id>.parquet
```

Each timeseries parquet contains one row per weather timestamp and includes the following columns:

#### Location and Geometry
- `suitable_area_id` — polygon identifier
- `latitude`, `longitude` — centroid WGS84 coordinates
- `slope_mean`, `aspect_mean` — surface tilt and stored GRASS-derived aspect
- `effective_area_m2` — suitable-area footprint

#### Solar Position and Horizon Context
- `timestamp` — UTC timestamp
- `solar_azimuth` — pvlib solar azimuth angle (degrees, clockwise from North)
- `solar_elevation` — solar elevation angle (degrees)
- `horizon_elevation` — merged pvlib/PVGIS-convention horizon profile elevation at the solar azimuth (degrees)

#### Input Weather
- `dni` — Direct Normal Irradiance (W/m²)
- `dhi` — Diffuse Horizontal Irradiance (W/m²)
- `ghi` — Global Horizontal Irradiance (W/m²)
- `temp_air` — ambient air temperature (°C)
- `wind_speed` — wind speed (m/s)

#### Irradiance After Horizon Shading
- `dni_adjusted` — DNI after applying horizon shading (horizon blocks direct beam when `solar_elevation <= horizon_elevation`)
- `poa_global` — plane-of-array global irradiance on the tilted surface (W/m²)

#### PV Parameters and Outputs
- `power_density_W_per_m2` — installed DC power density (W/m²)
- `gamma_pdc` — temperature coefficient for DC power
- `pdc0_w` — reference DC power at standard test conditions (W)
- `p_dc_w` — instantaneous DC power output (W)
- `temp_model` — temperature model used (`sapm` or `pvsyst`)
- `temp_class` — thermal mounting class
- `temp_cell` — photovoltaic cell temperature (°C)
- `output_mode` — output mode (`dc` or `dc_ac`)

#### Inverter Parameters (if `output_mode = dc_ac`)
- `dc_ac_ratio` — DC/AC ratio
- `eta_inv_nom` — inverter nominal efficiency
- `eta_inv_ref` — inverter reference efficiency
- `pac0_w` — inverter rated AC power (W)
- `p_ac_w` — instantaneous AC power output (W)

#### Notes

These files are the final persisted outputs of this branch. Merged horizon profiles are kept in memory for the simulation step and are not documented as a separate final on-disk artifact. In memory, merged horizon profiles are expressed in pvlib/PVGIS azimuth convention after converting the local GRASS `r.horizon` directions.

**Important distinction from real-sky batch**: The fast-horizon branch does NOT produce batch-model specific columns like `timestamp_clear`, `dni_clear`, `dhi_clear`, `ghi_clear`, `k_dir`, `k_diff`, or area-aggregated irradiance values. The output schema is specifically designed for the horizon-aware per-area simulation method.

### How It Differs from Real-Sky Batch

| Aspect | Real-Sky Batch | Fast Horizon |
|--------|---|---|
| **Prerequisite data** | `area_clearsky_means` (precomputed clear-sky area aggregates) + `suitable_areas` | existing horizon-analysis rasters + `suitable_areas` |
| **Simulation method** | scales precomputed clear-sky means with weather input using weighted area logic | centroid-based, horizon-aware pvlib simulation per area |
| **Solar position** | computed from single location | computed from each area's centroid |
| **Horizon handling** | implicit (via area-means precomputation) | explicit (merged local + external profiles; per-timestep blocking) |
| **Output layout** | partitioned Parquet (by `timestamp_clear`) with many areas per partition | one Parquet timeseries file per suitable area |
| **Output schema** | includes batch-specific columns: `timestamp_clear`, `dni_clear`, `dhi_clear`, `ghi_clear`, `k_dir`, `k_diff`, `beam_clear_area_mean`, `diff_clear_area_mean` | uses pvlib-aligned columns: `solar_azimuth`, `solar_elevation`, `horizon_elevation`, `dni_adjusted`, `poa_global` |

---

## Co-Simulation (CLI)

Co-simulation (`run_cosim.py`) runs photovoltaic co-simulation for one or more `suitable_area_id` values using the **batch model** logic. It is NOT available for the fast-horizon branch.

Uses:

- the `suitable_areas` dataset
- the `area_clearsky_means` dataset
- a real weather input for a specific timestamp

The script can run in two modes:

- `session-based`: reads context from Valkey for an existing backend session
- `manual paths`: reads datasets directly from disk

### Start Command

Always start from the repository root:

```bash
cd <PROJECT_ROOT>
python backend/scripts/run_cosim.py
```

### Before You Start

Make sure these prerequisite data products already exist for the target `location` and `mapset`:

- `suitable_areas.parquet`
- `area_clearsky_means` outputs

Typical storage layout:

```text
data/actinia-data/userdata/suitable_areas/<location>/<mapset>/suitable_areas.parquet
data/actinia-data/userdata/area_clearsky_means/<location>/<mapset>/
```

### Mode 1: Session-Based

Choose this when the target session was created and used through the backend application, and the required context has already been stored in Valkey.

Example:

```text
Choose context mode (1=session-based (recommended), 2=manual paths) [1]: 1
User ID: 1
Session ID: 1
```

This mode resolves `location`, `mapset`, `suitable_areas` context, and `area_clearsky_means` context automatically.

If you see:

```text
Error: suitable_areas context missing: suitable_areas_context not found in Valkey.
```

it means the datasets may exist on disk but the session metadata is not available in Valkey. Switch to `manual paths` mode.

### Mode 2: Manual Paths

Choose this when:

- you already know the dataset locations on disk
- the session context is missing from Valkey
- you want full control over `location` and `mapset`

Example interactive run:

```text
Choose context mode (1=session-based (recommended), 2=manual paths) [1]: 2
Suitable areas root [<PROJECT_ROOT>/data/actinia-data/userdata/suitable_areas]:
Area clear-sky means root [<PROJECT_ROOT>/data/actinia-data/userdata/area_clearsky_means]:
Location: <LOCATION_NAME>
Mapset [session_<SESSION_ID>]: session_<SESSION_ID>
Timestamp (ISO8601 with timezone) [<ISO8601_TIMESTAMP>]:
DNI [<DNI_VALUE>]: <DNI_VALUE>
DHI [<DHI_VALUE>]: <DHI_VALUE>
Air temperature (C) [<TEMP_AIR_C>]: <TEMP_AIR_C>
Wind speed [<WIND_SPEED>]: <WIND_SPEED>
Choose area mode (1=single area, 2=multiple specific areas, 3=all available areas) [3]: 3
```

### Choosing Areas

The script supports three area selection modes:

- `single area`
- `multiple specific areas`
- `all available areas`

### Non-Interactive Examples

#### Session-Based Example

```bash
cd <PROJECT_ROOT>
python backend/scripts/run_cosim.py \
  --user-id <USER_ID> \
  --session-id <SESSION_ID> \
  --timestamp "<ISO8601_TIMESTAMP>" \
  --dni <DNI_VALUE> \
  --dhi <DHI_VALUE> \
  --temp-air <TEMP_AIR_C> \
  --wind-speed <WIND_SPEED> \
  --all-areas
```

#### Manual Paths Example

```bash
cd <PROJECT_ROOT>
python backend/scripts/run_cosim.py \
  --suitable-areas-root <PROJECT_ROOT>/data/actinia-data/userdata/suitable_areas \
  --hot-root <PROJECT_ROOT>/data/actinia-data/userdata/area_clearsky_means \
  --location <LOCATION_NAME> \
  --mapset session_<SESSION_ID> \
  --timestamp "<ISO8601_TIMESTAMP>" \
  --dni <DNI_VALUE> \
  --dhi <DHI_VALUE> \
  --temp-air <TEMP_AIR_C> \
  --wind-speed <WIND_SPEED> \
  --all-areas
```

### Required Runtime Inputs

The script always asks for:

- `timestamp` — must be ISO8601 with timezone offset, e.g. `2026-01-15T12:00:00+00:00`
- `dni`
- `dhi`
- `temp_air`
- `wind_speed`

### Shared PV Model Parameters

Both the CLI co-simulation path and the batch REST path expose the same main PV-model parameters:

- `power_density_W_per_m2`
- `output_mode`
- `dc_ac_ratio`
- `eta_inv_nom`
- `eta_inv_ref`
- `temp_model`
- `temp_class`

In the CLI, these appear as:

- `--power-density`
- `--output-mode`
- `--dc-ac-ratio`
- `--eta-inv-nom`
- `--eta-inv-ref`
- `--temp-model`
- `--temp-class`

For multi-area co-simulation, the CLI also accepts optional `--ray-num-cpus`.

### Output Shape

#### Single Area

For a single area, the script returns a detailed result dictionary with fields such as:

- `suitable_area_id`
- `timestamp_real`
- `timestamp_clear`
- `effective_area_m2`
- `gti_area`
- `p_dc_w`
- `p_ac_w`
- `temp_cell`

#### Multiple Areas

For multiple areas, the script defaults to `summary_by_area`. Example:

```json
{
  "<AREA_ID_1>": {
    "area_id": "<AREA_ID_1>",
    "timestamp_real": "<ISO8601_TIMESTAMP>",
    "timestamp_clear": "<CLEAR_SKY_SLOT>",
    "p_ac_kw": 3.529,
    "p_dc_kw": 3.669,
    "gti_area": 673.95,
    "effective_area_m2": 31.03,
    "temp_cell": 33.52
  }
}
```

---

## Common Problems

### `unsupported_weather_file_type`

Cause:

- the uploaded batch weather file is not one of the supported extensions: `.csv`, `.parquet`, `.pq`, or `.epw`

### `invalid_weather_columns_json`

Cause:

- `weather_columns_json` was not valid JSON or was not a JSON object

### `weather_columns_missing`

Cause:

- one or more required logical weather fields were missing from `weather_columns_json`

### `invalid_output_mode`

Cause:

- `output_mode` was not `dc` or `dc_ac`

### `invalid_temp_model`

Cause:

- `temp_model` was not `sapm` or `pvsyst`

### `invalid_temp_class_for_sapm` / `invalid_temp_class_for_pvsyst`

Cause:

- `temp_class` did not match the selected temperature-model family

### Missing Prerequisite Datasets

Cause:

- a later stage was run before an earlier stage produced the required inputs

### `suitable_areas_context not found in Valkey` (co-sim)

Fix:

- switch to `manual paths` mode
