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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Use this workflow when the user has an existing data model in their own system a
- **api.py**: HTTP client layer (`Client`, `Response` dataclass). Handles authentication, timeouts, error mapping.
- **endpoint_*.py**: Feature modules (About, Service, ModelChain, TerminateAsync). Each exports one main function.
- **weather.py**: Meteorological file format specification and conversion utilities. Exports `TSV_COLUMNS` (format spec), `from_dataframe()`, `from_pvlib()`, `from_solcast()` (converters from common data sources), and `check_sequential_year_timestamps()` (TMY validator). Use when users need to convert weather data from pandas, pvlib, or Solcast into SolarFarmer's TSV format.
- **custom_rotations.py**: CSV import and protobuf serialization for custom tracker rotation schedules. Exports `from_csv()`, `csv_to_protobuf()`, and `validate_tracker_rotation_ids()`. Datasets that exceed `max_timesteps_per_file` (default 40,000) are automatically split into numbered `*001of002.gz` files. Use when users need to supply a custom rotation schedule to a 3D calculation.
- **models/**: Two distinct kinds of model:
- **Pydantic models** (`SolarFarmerBaseModel` subclasses, `frozen=True`): `EnergyCalculationInputs`, `PVPlant`, `Location`, `Inverter`, `Layout`, `Transformer`, etc. These are **immutable** — mutations raise `ValidationError`. Serialize with `model_dump(by_alias=True, exclude_none=True)`.
- **Results models**: `CalculationResults` (in `energy_calculation_results.py`) wraps API outputs and provides convenience properties and accessors such as `performance_ratio_bifacial`, `get_performance()`, `print_annual_results()`, `loss_tree_timeseries()`, and `pvsyst_timeseries()`.
Expand Down Expand Up @@ -190,6 +191,7 @@ The following are **named workflows** for structuring developer work. They are N
| Add polling timeout logic | EndpointDev workflow | Requires config constants, async pattern |
| Help SDK user run a calculation | Default referencing Quickstart | Refer to copilot-instructions.md quickstart |
| Convert weather data to SF format | Default | Use `sf.from_dataframe()`, `sf.from_pvlib()`, or `sf.from_solcast()` |
| Convert custom tracker rotation CSV to protobuf | Default | Use `sf.custom_rotations.from_csv()` + `to_protobuf_file()`, or one-step `sf.custom_rotations.csv_to_protobuf()` |

## Tool Restrictions

Expand Down
40 changes: 40 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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`)
- [**Custom Tracker Rotations**](#custom-tracker-rotations): Convert CSV rotation schedules to protobuf format for 3D calculations

### Configuration & Design

Expand Down Expand Up @@ -105,6 +106,45 @@ Data dictionary describing the SolarFarmer TSV weather file format: required and

---

## Custom Tracker Rotations

Convert a CSV schedule of tracker rotation angles to the gzip-compressed
protobuf file required by SolarFarmer 3D calculations.

### `custom_rotations.from_csv()`

::: solarfarmer.custom_rotations.from_csv
options:
extra:
show_root_toc_entry: false
show_root_members: true

### `custom_rotations.from_csv_folder()`

::: solarfarmer.custom_rotations.from_csv_folder
options:
extra:
show_root_toc_entry: false
show_root_members: true

### `custom_rotations.csv_to_protobuf()`

::: solarfarmer.custom_rotations.csv_to_protobuf
options:
extra:
show_root_toc_entry: false
show_root_members: true

### `custom_rotations.validate_tracker_rotation_ids()`

::: solarfarmer.custom_rotations.validate_tracker_rotation_ids
options:
extra:
show_root_toc_entry: false
show_root_members: true

---

## Main Classes

The core classes handle the complete workflow from plant design to results analysis:
Expand Down
172 changes: 172 additions & 0 deletions docs/getting-started/custom-tracker-rotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
title: Custom Tracker Rotations
description: Convert and validate custom tracker rotation CSV schedules
---

# Custom Tracker Rotations

Use a CSV schedule to create the gzip-compressed protobuf file required for
custom tracker rotations in a SolarFarmer 3D calculation.

## Convert a CSV Schedule

```python
import solarfarmer as sf

dataset = sf.custom_rotations.from_csv(
"tracker_rotations.csv",
offset_from_utc=1.0,
rotations_are_at_middle_of_period=False,
)
written = dataset.to_protobuf_file("TrackersConditionsDatasetDto_Protobuf.gz")
```

`to_protobuf_file` returns a list of the paths it wrote. Datasets with up to
40,000 timesteps produce a single file (`TrackersConditionsDatasetDto_Protobuf.gz`).
Larger datasets are split automatically into numbered parts:

```
TrackersConditionsDatasetDto_Protobuf001of002.gz
TrackersConditionsDatasetDto_Protobuf002of002.gz
```

You can override the split threshold:

```python
dataset.to_protobuf_file("TrackersConditionsDatasetDto_Protobuf.gz", max_timesteps_per_file=20_000)
```

For a one-step conversion, use:

```python
import solarfarmer as sf

sf.custom_rotations.csv_to_protobuf(
"tracker_rotations.csv",
"TrackersConditionsDatasetDto_Protobuf.gz",
offset_from_utc=1.0,
)
```

Place the resulting file(s) beside the calculation JSON and other input files.
`run_energy_calculation(inputs_folder_path=...)` discovers both the single-file
and the multi-part naming patterns automatically.

## Converting CSV Files in a Folder

When the rotation schedule is split across multiple CSV files (for example one
file per month or per week), use `from_csv_folder` instead of `from_csv`. It
reads every `*.csv` file in the directory, validates each one independently,
sorts them by their first timestamp regardless of filename order, and merges
them into a single dataset:

```python
import solarfarmer as sf

dataset = sf.custom_rotations.from_csv_folder(
"rotation_csvs/",
offset_from_utc=1.0,
rotations_are_at_middle_of_period=False,
)
written = dataset.to_protobuf_file("TrackersConditionsDatasetDto_Protobuf.gz")
```

All files must contain **exactly the same tracker columns in the same order**.
The importer raises `ValueError` if any file has different or reordered IDs,
overlapping timestamps, or an inconsistent time period compared with the other
files. All keyword arguments accepted by `from_csv` (`offset_from_utc`,
`flip_sign`, `energy_calculation_inputs`, etc.) are forwarded to every file.

## CSV Format

The CSV has separate local timestamp fields followed by one column for each
tracker rotation ID:

```text
Year,Month,Day,Hour,Minute,Second,Tracker0,Tracker1
2025,1,1,8,0,0,-1.5,-1.5
2025,1,1,8,5,0,-3.0,-3.0
```

`Year`, `Month`, `Day`, `Hour`, and `Minute` are required. `Second` is
optional. `Azimuth` and `Zenith` are accepted only for compatibility with old
exports and are ignored. Tracker rotation IDs are matched case-insensitively,
so the CSV cannot contain both `Group1` and `group1`.

The importer validates calendar timestamps, numeric finite angles, the valid
range from -89.90 to 89.90 degrees, duplicate headers, and strictly increasing
timestamps. It infers the base period from the timestamps; a larger gap is
allowed when a CSV contains daytime rotations only, and every output record
uses the inferred base period.

Timestamps use the fixed `offset_from_utc` supplied to the importer. No
daylight-saving-time adjustment is applied.

## Rotation Direction

SolarFarmer expects negative angles in the morning and positive angles in the
afternoon. If a schedule clearly has the opposite convention, the importer
emits a warning. Reverse it explicitly when needed:

```python
dataset = sf.custom_rotations.from_csv(
"tracker_rotations.csv",
offset_from_utc=1.0,
flip_sign=True,
)
```

## Validate Against Calculation Inputs

When the calculation payload is available, check that the CSV tracker IDs
match the `trackerRotationID` values in its `pvPlant.trackers` collection.
Pass the JSON file path directly — the SDK loads and parses it automatically:

```python
from pathlib import Path
import solarfarmer as sf

dataset = sf.custom_rotations.from_csv(
"tracker_rotations.csv",
offset_from_utc=1.0,
energy_calculation_inputs=Path("EnergyCalculationInputs.json"),
)
```

You can also pass a pre-loaded dict or `EnergyCalculationInputs` object instead
of a path. The check is optional because a CSV can be prepared before the full
payload is available. IDs are compared case-insensitively but the original CSV
spelling is preserved in the generated protobuf file.

## Things to Verify Before Running

The following checks are not enforced by the SDK but are worth confirming
before submitting a calculation.

**Rotation timestamps use the same fixed UTC offset as the weather file.**
The importer applies a fixed `offset_from_utc` to all timestamps; DST
transitions are not applied. Use the same offset you supplied to the weather
file converter.

**The weather data covers the full simulation period.**
The tracker rotation timestamps define the simulation period. If a full-year
weather file is provided but the rotation data covers only three months,
SolarFarmer will simulate those three months only. Verify that weather data
is available for every timestamp in the rotation schedule.

```python
# weather_timestamps: list of datetime objects from your weather source
# (e.g. df.index.to_pydatetime() for a pandas DataFrame)
sf.custom_rotations.check_weather_covers_rotation_period(weather_timestamps, dataset)
```

**The weather and rotation time resolutions are compatible.**
Both files may share the same resolution (e.g. 5-minute weather and
5-minute rotations) or use proportional resolutions (e.g. hourly weather
and 5-minute rotations). In the latter case SolarFarmer interpolates the
weather data to match the rotation timestamps. Be aware that interpolated
irradiance values may differ slightly from the original data.

```python
sf.custom_rotations.check_compatible_time_resolutions(weather_timestamps, dataset)
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ nav:
- Workflow 1 - Load Existing API Files: getting-started/workflow-1-existing-api-files.md
- Workflow 2 - Design Plants with PVSystem: getting-started/workflow-2-pvplant-builder.md
- Workflow 3 - Advanced Integration: getting-started/workflow-3-plantbuilder-advanced.md
- Custom Tracker Rotations: getting-started/custom-tracker-rotations.md
- Quick Start Examples: getting-started/quick-start-examples.md
- End-to-End Examples: getting-started/end-to-end-examples.md
- API Reference: api.md
Expand Down
16 changes: 16 additions & 0 deletions solarfarmer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import custom_rotations
from .__version__ import __version__
from .api import SolarFarmerAPIError
from .config import (
Expand All @@ -21,6 +22,16 @@
SF_PORTAL_URL,
TERMINATE_ASYNC_ENDPOINT_URL,
)
from .custom_rotations import (
RotationSignConventionWarning,
validate_tracker_rotation_ids,
)
from .custom_rotations import (
csv_to_protobuf as custom_rotations_csv_to_protobuf,
)
from .custom_rotations import (
from_csv as from_custom_rotations_csv,
)
from .endpoint_about import about
from .endpoint_modelchains import run_energy_calculation
from .endpoint_service import service
Expand Down Expand Up @@ -87,8 +98,11 @@

__all__ = [
"__version__",
"custom_rotations",
"custom_rotations_csv_to_protobuf",
"about",
"configure_logging",
"from_custom_rotations_csv",
# Config constants
"ABOUT_ENDPOINT_URL",
"ANNUAL_MONTHLY_RESULTS_FILENAME",
Expand Down Expand Up @@ -168,4 +182,6 @@
"from_pvlib",
"from_solcast",
"check_sequential_year_timestamps",
"RotationSignConventionWarning",
"validate_tracker_rotation_ids",
]
Loading