Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
uv-solarfarmer-

- name: Install documentation dependencies
run: uv sync --extra docs
run: uv sync --group docs

- name: Determine version from git
id: version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
cache-dependency-glob: "pyproject.toml"

- name: Install dependencies
run: uv sync --extra dev
run: uv sync --group dev

- name: Run Ruff linter
run: uv run ruff check solarfarmer/ tests/ --output-format=github
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --extra test
run: uv sync --group test

- name: Run tests
run: uv run pytest tests/ -v
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@
[![CI](https://github.com/dnv-opensource/solarfarmer-python-sdk/actions/workflows/test.yml/badge.svg)](https://github.com/dnv-opensource/solarfarmer-python-sdk/actions/workflows/test.yml)
[![Documentation](https://img.shields.io/badge/docs-online-teal)](https://dnv-opensource.github.io/solarfarmer-python-sdk/)

The official Python SDK for [DNV SolarFarmer](https://www.dnv.com/software/services/solarfarmer/), a bankable solar PV calculation engine. Use it to build validated API payloads, run cloud-based 2D and 3D energy calculations, and analyse simulation results — all from Python.
The official Python SDK for [SolarFarmer](https://www.dnv.com/software/services/solarfarmer/), a bankable solar PV design and energy yield assessment software from DNV. This SDK provides a typed Python interface that simplifies calling SolarFarmer APIs: build payloads, run 2D and 3D energy calculations, and process results programmatically.

## Key Features

- **API-faithful data models** that closely mirror the SolarFarmer API schema, with serialization conveniences (snake_case Python fields serialized to the correct camelCase JSON automatically) to reduce integration friction
- **Two plant-building approaches:** a bottom-up route using `EnergyCalculationInputs`, `PVPlant`, and component classes (`Inverter`, `Layout`, `Transformer`, etc.) for full control over the plant topology; and a `PVSystem` convenience class that accepts high-level parameters (capacity, tilt, GCR, equipment files) and constructs the payload automatically, suited to indicative simulations where exhaustive detail is not required
- **`CalculationResults` encapsulation** — the API response is wrapped in a `CalculationResults` object that provides structured access to annual and monthly energy metrics, loss trees, PVsyst-format time-series, and detailed time-series output without manual JSON parsing
- **ModelChain and ModelChainAsync endpoint support** — the SDK dispatches to the synchronous `ModelChain` endpoint or the asynchronous `ModelChainAsync` endpoint as appropriate, and handles polling automatically for long-running jobs
- **Async job management** — poll, monitor, and terminate async calculations via `terminate_calculation()`
- **Data models that mirror the API schema.** Pydantic classes with field validation catch payload errors locally before the API call. Field descriptions and type hints improve discoverability.
- **Two plant-building paths.** Full control via `EnergyCalculationInputs` and component classes, or quick screening via `PVSystem` from high-level specs (DC and AC capacities, tilt, GCR)
- **Structured results.** `CalculationResults` gives direct access to annual/monthly metrics, loss trees, and time series without parsing raw JSON.
- **Automatic endpoint handling.** One function call runs 2D or 3D calculations. The SDK selects the right endpoint, polls async jobs, and supports cancellation via `terminate_calculation()`.

## Requirements

- Python >= 3.10 (tested on 3.10, 3.11, 3.12, 3.13)
- A SolarFarmer API key (commercial licence required) — see [API Key](#api-key)
- A SolarFarmer API key (commercial licence required; see [API Key](#api-key))

## Installation

Expand All @@ -38,9 +37,10 @@ import solarfarmer as sf
Install with optional extras:

```bash
pip install "dnv-solarfarmer[weather]" # pandas for weather file conversion and DataFrame results
pip install "dnv-solarfarmer[notebooks]" # JupyterLab and notebook support
pip install "dnv-solarfarmer[all]" # full installation including pandas and matplotlib
pip install "dnv-solarfarmer[dev]" # linting and testing tools (for contributors)
pip install "dnv-solarfarmer[all]" # full installation including pandas and matplotlib
pip install "dnv-solarfarmer[dev]" # linting and testing tools (for contributors)
```

Install from source:
Expand All @@ -67,24 +67,35 @@ Alternatively, pass it directly as the `api_key` parameter to any function that

| Environment Variable | Default | Description |
|---|---|---|
| `SF_API_KEY` | *(none required for calculations)* | API authentication token |
| `SF_API_KEY` | *(none; required for calculations)* | API authentication token |
| `SF_API_URL` | `https://solarfarmer.dnv.com/latest/api` | Override the base API URL for custom deployments |

## Optional Dependencies

The core SDK (`pydantic`, `requests`, `tabulate`) does not depend on `pandas`.
Install the `weather` extra for DataFrame-based features:

```bash
pip install "dnv-solarfarmer[weather]"
```

This unlocks `sf.from_dataframe()` and `sf.from_pvlib()` for writing weather files from DataFrames, and enables `CalculationResults` to parse timeseries outputs into DataFrames. Without pandas, those functions raise `ImportError` or return `None`. All other SDK features work without it.

## Getting Started

The SDK is built around three workflows suited to different use cases:
The SDK supports three workflows for different use cases:

| Workflow | Best for | Primary entry point |
| Workflow | Best for | Entry point |
|---|---|---|
| 1. Load existing files | Users with pre-built API payloads from the SolarFarmer desktop app or a previous export | `sf.run_energy_calculation(inputs_folder_path=...)` |
| 2. PVSystem builder | Solar engineers designing new plants programmatically with automatic payload generation | `sf.PVSystem(...)` then `plant.run_energy_calculation()` |
| 3. Custom integration | Developers mapping internal databases or proprietary formats to the SolarFarmer API | `sf.EnergyCalculationInputs(location=..., pv_plant=..., ...)` |
| 2. PVSystem builder | Quick screening from high-level specs (capacity, tilt, equipment files). The design is approximate: string sizing and inverter count are inferred, so DC/AC capacity may not match the target exactly. | `plant = sf.PVSystem(...)` then `plant.run_energy_calculation()` |
| 3. Custom integration | Developers mapping internal databases or proprietary formats to the SolarFarmer API | `params = sf.EnergyCalculationInputs(...)` then `sf.run_energy_calculation(plant_builder=params)` |

See the [Getting Started guide](https://dnv-opensource.github.io/solarfarmer-python-sdk/getting-started/) for full per-workflow walkthroughs, and the [example notebooks](https://dnv-opensource.github.io/solarfarmer-python-sdk/notebooks/Example_EnergyCalculations/) for runnable end-to-end examples.

## Documentation

Full documentation including API reference, workflow guides, and notebook tutorials:
Full documentation (API reference, workflow guides, notebook tutorials):

**https://dnv-opensource.github.io/solarfarmer-python-sdk/**

Expand Down
50 changes: 50 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ The SolarFarmer SDK is organized into the following main categories:

- [**Endpoint Functions**](#endpoint-functions): Core functions for making API calls
- [**Main Classes**](#main-classes): Key data models for calculations and plant design
- [**Weather Utilities**](#weather-utilities): Convert DataFrames to SolarFarmer weather files (requires `pandas`)

### Configuration & Design

- [**Plant Configuration Classes**](#plant-configuration-classes): Location, layout, and inverter specifications
- [**Module and Equipment Specifications**](#module-and-equipment-specifications): Mounting types, trackers, and component files
- [**Enums**](#enums): PVSystem configuration enums (`MountingType`, `InverterType`, `OrientationType`)

### Advanced Options

Expand Down Expand Up @@ -68,6 +70,41 @@ These are the primary functions for interacting with the SolarFarmer API.

---

## Weather Utilities

!!! note
These functions require `pandas`. Install with `pip install 'dnv-solarfarmer[weather]'`.

### `from_dataframe()`

::: solarfarmer.weather.from_dataframe
options:
extra:
show_root_toc_entry: false
show_root_members: true

### `from_pvlib()`

::: solarfarmer.weather.from_pvlib
options:
extra:
show_root_toc_entry: false
show_root_members: true

### `validate_tsv_timestamps()`

::: solarfarmer.weather.validate_tsv_timestamps
options:
extra:
show_root_toc_entry: false
show_root_members: true

### `TSV_COLUMNS`

Data dictionary describing the SolarFarmer TSV weather file format — required and optional columns, units, valid ranges, aliases, and the missing-value sentinel. See the [`weather` module docstring](../api.md) for full details.

---

## Main Classes

The core classes handle the complete workflow from plant design to results analysis:
Expand Down Expand Up @@ -188,6 +225,19 @@ The core classes handle the complete workflow from plant design to results analy

---

## Enums

These enums are used with `PVSystem` to configure mounting, inverter, and module orientation.
They are available at the top level: `sf.MountingType`, `sf.InverterType`, `sf.OrientationType`.

| Enum | Values | Used by |
|---|---|---|
| `MountingType` | `FIXED`, `TRACKER` | `PVSystem.mounting` |
| `InverterType` | `CENTRAL`, `STRING` | `PVSystem.inverter_type` |
| `OrientationType` | `PORTRAIT`, `LANDSCAPE` | `PVSystem.module_orientation` |

---

## Version Information

::: solarfarmer.__version__
Expand Down
20 changes: 20 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,23 @@ Yes! Use the **"Import from API JSON"** feature in SolarFarmer Desktop. See the
- Uses simplified row positioning (all strings in middle-row assumption)

For information about upcoming features or requests, contact [solarfarmer@dnv.com](mailto:solarfarmer@dnv.com).

---

## Why do I get an `ImportError` when calling `from_dataframe()` or `from_pvlib()`?

These weather conversion functions require `pandas`, which is an optional dependency. Install it with:

```bash
pip install "dnv-solarfarmer[weather]"
```

The core SDK (payload construction, API calls, annual/monthly summary data) works without pandas. Only the weather file conversion utilities and timeseries result parsing need it. See the [Weather Utilities reference](api.md#weather-utilities) for details.

---

## My TMY weather file gives a 400 error with no useful message. What's wrong?

TMY (Typical Meteorological Year) datasets from NSRDB, PVGIS, or similar sources contain timestamps from multiple source years. SolarFarmer requires all timestamps in a TSV file to belong to a single calendar year.

Use [`sf.from_pvlib()`](api.md#from_pvlib) or [`sf.from_dataframe(year=1990)`](api.md#from_dataframe) to remap timestamps automatically. The SDK also calls [`validate_tsv_timestamps()`](api.md#validate_tsv_timestamps) before upload to catch this early.
9 changes: 9 additions & 0 deletions docs/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ The SolarFarmer SDK supports three distinct user workflows. Choose the one that

Once you have your API key, provide it to the SDK using either the **SF_API_KEY** environment variable or pass it directly as the `api_key` argument when calling the energy calculation function.

!!! tip "Optional: pandas for weather conversion and timeseries results"

The core SDK works without pandas. To use `sf.from_dataframe()`, `sf.from_pvlib()`,
or to parse timeseries results as DataFrames, install the `weather` extra:

```bash
pip install "dnv-solarfarmer[weather]"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think weather will not age well. It's already awkward with how the response parsing also optionally uses pandas. One "kick the can down the road" solution is putting it into an all dependency while also moving the dev/test/docs dependencies into dependency groups

```

## Choose Your Path

### [Workflow 1: Load and Execute Existing API Files](workflow-1-existing-api-files.md)
Expand Down
14 changes: 6 additions & 8 deletions docs/getting-started/workflow-1-existing-api-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,15 @@ results.describe()
### Access Key Metrics

```python
# Access annual data for the calculation period
annual_data = results.AnnualData[0]
# Convenience properties (year 1)
print(f"Net Energy: {results.net_energy_MWh:.1f} MWh/year")
print(f"Performance Ratio: {results.performance_ratio:.3f}")
print(f"Specific Yield: {results.energy_yield_kWh_per_kWp:.1f} kWh/kWp")

# Get net energy yield (MWh/year)
# Or access full annual data dicts directly
annual_data = results.AnnualData[0]
net_energy_mwh = annual_data['energyYieldResults']['netEnergy']

# Get performance ratio (%)
performance_ratio = annual_data['energyYieldResults']['performanceRatio']

print(f"Net Energy: {net_energy_mwh} MWh/year")
print(f"Performance Ratio: {performance_ratio}%")
```

### Access Simulation Results
Expand Down
25 changes: 21 additions & 4 deletions docs/getting-started/workflow-2-pvplant-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,22 @@ plant.grid_limit_MW = 4.8 # Grid connection limit
```python
# Mounting and orientation
plant.mounting = "Fixed" # or "Tracker" for single-axis trackers
# also available as sf.MountingType.FIXED / sf.MountingType.TRACKER
plant.tilt = 25.0 # Array tilt in degrees
plant.azimuth = 180.0 # South-facing (0=North, 90=East, 180=South, 270=West)
plant.gcr = 0.4 # Ground coverage ratio (spacing between rows)
plant.flush_mount = False # Flush-mounted or rack-mounted

# Module orientation
plant.module_orientation = "Portrait" # or "Landscape"
plant.module_orientation = "Portrait" # or "Landscape" (sf.OrientationType)
plant.modules_across = 1 # Number of modules in height direction
```

### Equipment Selection

```python
# Inverter type affects default losses
plant.inverter_type = "Central" # or "String" inverters
plant.inverter_type = "Central" # or "String" inverters (sf.InverterType)

# Transformer configuration
plant.transformer_stages = 1 # 0 (ideal) or 1 (with losses)
Expand Down Expand Up @@ -124,7 +125,8 @@ plant.bifacial_mismatch_loss = 0.01

## Step 3: Add Module and Inverter Files

The SDK uses PAN (module) and OND (inverter) files for detailed specifications:
The SDK uses PAN (module) and OND (inverter) files for detailed specifications.
Dict keys are user-facing labels only — the spec ID sent to the API is derived from the filename (everything before the last `.`).

```python
from pathlib import Path
Expand All @@ -138,12 +140,22 @@ plant.add_pan_files({
plant.add_ond_files({
"My_Inverter": Path(r"path/to/inverter.OND")
})

# Or pass a list of paths (keys are derived from filenames)
plant.pan_files = [Path(r"path/to/module.PAN")]
plant.ond_files = [Path(r"path/to/inverter.OND")]
```

---

## Step 4: Add Weather and Horizon Data

!!! warning "TMY data: remap timestamps to a single year"
TMY datasets from NSRDB, PVGIS, or similar sources contain timestamps from
multiple source years. When using TSV format, all timestamps must belong to a
single calendar year. Use `sf.from_pvlib()` or `sf.from_dataframe(year=1990)`
to handle this automatically, or remap manually before export.

```python
# Meteorological data (required for calculation)
plant.weather_file = Path(r"path/to/weather_data.csv")
Expand Down Expand Up @@ -234,7 +246,12 @@ Use the [`CalculationResults`](../api.md#calculationresults) class to access and
# Evaluate the results from the energy simulation
plant.results.performance()

# Access annual data
# Quick access to year-1 metrics via convenience properties
print(f"Net Energy: {plant.results.net_energy_MWh:.1f} MWh/year")
print(f"Performance Ratio: {plant.results.performance_ratio:.3f}")
print(f"Specific Yield: {plant.results.energy_yield_kWh_per_kWp:.1f} kWh/kWp")

# Or access annual data dicts directly
annual_data = plant.results.AnnualData[0]
net_energy = annual_data['energyYieldResults']['netEnergy']
performance_ratio = annual_data['energyYieldResults']['performanceRatio']
Expand Down
27 changes: 14 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ dependencies = [
]

[project.optional-dependencies]
notebooks = [
"ipykernel>=6.29,<8",
"jupyterlab>=4.0,<6",
"notebook>=7.0,<9"
]

all = [
"dnv-solarfarmer[notebooks]",
"pandas>=2.0",
"matplotlib>=3.5",
]

[dependency-groups]
docs = [
"zensical>=0.0.20",
"mkdocstrings[python]>=0.26.1",
Expand All @@ -46,23 +59,11 @@ test = [
]

dev = [
"dnv-solarfarmer[test]",
{include-group = "test"},
"pre-commit>=3.0",
"ruff>=0.5",
]

notebooks = [
"ipykernel>=6.29,<8",
"jupyterlab>=4.0,<6",
"notebook>=7.0,<9"
]

all = [
"dnv-solarfarmer[docs,dev,notebooks]",
"pandas>=2.0",
"matplotlib>=3.5",
]

[project.urls]
Homepage = "https://www.dnv.com/software/services/solarfarmer/"
Repository = "https://github.com/dnv-opensource/solarfarmer-python-sdk"
Expand Down
Loading
Loading