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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ classifiers = [

dependencies = [
"pydantic>=2.0",
"protobuf>=5.0",
"requests>=2.28",
"tabulate>=0.9.0",
]
Expand Down
6 changes: 6 additions & 0 deletions solarfarmer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@
TerrainRowDto,
TerrainRowStartEndColumnsDto,
Tracker,
TrackerAlgorithm,
TrackerCondition,
Trackers,
TrackersConditionsDataset,
TrackerSystem,
Transformer,
TransformerLossModelTypes,
Expand Down Expand Up @@ -150,8 +153,11 @@
"TerrainRowStartEndColumnsDto",
"terminate_calculation",
"Tracker",
"TrackerAlgorithm",
"TrackerCondition",
"TrackerSystem",
"Trackers",
"TrackersConditionsDataset",
"TSV_COLUMNS",
"Transformer",
"TransformerLossModelTypes",
Expand Down
4 changes: 4 additions & 0 deletions solarfarmer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"PVSYST_TIMESERIES_FILENAME",
"PVSYST_TIMESERIES_DATAFRAME_FILENAME",
"DETAILED_TIMESERIES_FILENAME",
"TRACKER_INCIDENCE_ANGLES_FILENAME",
"TRACKER_ROTATION_ANGLES_FILENAME",
"GENERAL_TIMEOUT",
"MODELCHAIN_TIMEOUT",
"MODELCHAIN_ASYNC_TIMEOUT_CONNECTION",
Expand All @@ -42,6 +44,8 @@
PVSYST_TIMESERIES_FILENAME = "PVsystResults.csv"
PVSYST_TIMESERIES_DATAFRAME_FILENAME = "PVsystResults_frame.csv"
DETAILED_TIMESERIES_FILENAME = "DetailedTimeseries.tsv"
TRACKER_INCIDENCE_ANGLES_FILENAME = "TrackerPositions_IncidenceAngles.csv"
TRACKER_ROTATION_ANGLES_FILENAME = "TrackerPositions_RotationAngles.csv"

# Default times (in seconds)
GENERAL_TIMEOUT = 15 # Used in About, Service endpoints
Expand Down
16 changes: 16 additions & 0 deletions solarfarmer/endpoint_modelchains.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def _resolve_request_payload(
pan_file_paths: list[str] | None,
ond_file_paths: list[str] | None,
plant_builder: str | SolarFarmerBaseModel | None,
tracker_rotation_paths: list[str] | None = None,
) -> tuple[str, list[tuple[str, IO[bytes]]]]:
"""
Resolve the API request payload and associated input files.
Expand All @@ -123,6 +124,10 @@ def _resolve_request_payload(
Paths to OND inverter files
plant_builder : str or SolarFarmerBaseModel or None
Pre-built payload as a model instance or JSON string
tracker_rotation_paths : list of str or None, optional
Paths to ``TrackersConditionsDatasetDto_Protobuf*.gz`` files.
Ignored when ``inputs_folder_path`` is used (files are discovered
automatically from the folder)

Returns
-------
Expand Down Expand Up @@ -155,6 +160,7 @@ def _resolve_request_payload(
pan_file_paths,
ond_file_paths,
energy_calculation_inputs_file_path,
tracker_rotation_paths=tracker_rotation_paths,
)
elif plant_builder is not None:
# Option 3: use the data from plant builder
Expand All @@ -165,6 +171,7 @@ def _resolve_request_payload(
ond_file_paths,
energy_calculation_inputs_file_path=None,
parse_energy_calc_inputs=False,
tracker_rotation_paths=tracker_rotation_paths,
)
# Ensure the request is a JSON string
if isinstance(plant_builder, SolarFarmerBaseModel):
Expand Down Expand Up @@ -319,6 +326,7 @@ def run_energy_calculation(
horizon_file_path: str | None = None,
ond_file_paths: list[str] | None = None,
pan_file_paths: list[str] | None = None,
tracker_rotation_paths: list[str] | None = None,
print_summary: bool = True,
outputs_folder_path: str | pathlib.Path | None = None,
save_outputs: bool = True,
Expand Down Expand Up @@ -366,6 +374,13 @@ def run_energy_calculation(
One or more paths to PAN module specification files. At least
one PAN file is required if not provided via ``modelchain_payload``
or ``folder_path``
tracker_rotation_paths : list of str, optional
One or more paths to ``TrackersConditionsDatasetDto_Protobuf*.gz``
files carrying custom tracker rotation data. Pass them in the
correct part order when using multi-part files (e.g.
``001of002`` before ``002of002``). When ``inputs_folder_path`` is
used, matching files are discovered automatically from the folder
and this parameter is ignored
print_summary : bool, optional
If True, it will print out the summary of the energy calculation results.
Default is True
Expand Down Expand Up @@ -441,6 +456,7 @@ def run_energy_calculation(
pan_file_paths,
ond_file_paths,
plant_builder,
tracker_rotation_paths,
)

# 2. Dispatch to the appropriate endpoint
Expand Down
32 changes: 32 additions & 0 deletions solarfarmer/endpoint_modelchains_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ def get_files(sample_data_folder: str | pathlib.Path) -> list[tuple[str, IO[byte
" Only a single meteorological file is supported."
)

# Look for TrackersConditionsDatasetDto_Protobuf*.gz files (custom tracker rotation data)
tracker_rotation_file_paths = sorted(
get_file_paths_in_folder(
sample_data_folder, "TrackersConditionsDatasetDto_Protobuf*.gz"
)
)
for tracker_rotation_file_path in tracker_rotation_file_paths:
_logger.debug("customRotationDataTransferFiles = %s", tracker_rotation_file_path)
fh = pathlib.Path(tracker_rotation_file_path).open("rb")
stack.callback(fh.close)
files.append(("customRotationDataTransferFiles", fh))

# Look for PAN files in the folder and add them
pan_file_paths = get_file_paths_in_folder(sample_data_folder, "*.PAN")
for pan_file_path in pan_file_paths:
Expand Down Expand Up @@ -266,6 +278,7 @@ def parse_files_from_paths(
ond_file_paths: list[str],
energy_calculation_inputs_file_path: str | None,
parse_energy_calc_inputs: bool = True,
tracker_rotation_paths: list[str] | None = None,
) -> tuple[str, list[tuple[str, IO[bytes]]]]:
"""
Parse input files for the ModelChain or ModelChainAsync call from explicit paths.
Expand All @@ -288,6 +301,12 @@ def parse_files_from_paths(
parse_energy_calc_inputs : bool, default True
If False, the JSON inputs file is not read and an empty string is
returned as the request content
tracker_rotation_paths : list of str or None, optional
Paths to one or more ``TrackersConditionsDatasetDto_Protobuf*.gz``
files carrying custom tracker rotation data. When provided, the
files are uploaded as ``customRotationDataTransferFiles``.
Pass them in the correct order when using multi-part files
(e.g. ``001of002`` before ``002of002``)

Returns
-------
Expand Down Expand Up @@ -369,6 +388,19 @@ def parse_files_from_paths(
else:
raise FileNotFoundError(f"Error: Path does not exist -> {ond_file_path}")

# Add any tracker rotation transfer files
if tracker_rotation_paths is not None:
for tracker_rotation_path in tracker_rotation_paths:
if path_exists(tracker_rotation_path):
_logger.debug("customRotationDataTransferFiles = %s", tracker_rotation_path)
fh = pathlib.Path(tracker_rotation_path).open("rb")
stack.callback(fh.close)
files.append(("customRotationDataTransferFiles", fh))
else:
raise FileNotFoundError(
f"Error: Path does not exist -> {tracker_rotation_path}"
)

if parse_energy_calc_inputs:
# Get the JSON energy calculation inputs from the input folder path
with pathlib.Path(energy_calculation_inputs_file_path).open("rb") as file:
Expand Down
5 changes: 5 additions & 0 deletions solarfarmer/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
MissingMetDataMethod,
OrderColumnsPvSystFormatTimeSeries,
PowerOptimizerOperationType,
TrackerAlgorithm,
TransformerLossModelTypes,
)
from .indexed_object3d import IndexedObject3D
Expand Down Expand Up @@ -44,6 +45,7 @@
from .tracker import Tracker
from .tracker_system import TrackerSystem
from .trackers import Trackers
from .trackers_conditions_dataset import TrackerCondition, TrackersConditionsDataset
from .transformer import Transformer
from .transformer_specification import TransformerSpecification
from .vector3double import Vector3Double
Expand Down Expand Up @@ -89,8 +91,11 @@
"TerrainRowDto",
"TerrainRowStartEndColumnsDto",
"Tracker",
"TrackerAlgorithm",
"TrackerCondition",
"TrackerSystem",
"Trackers",
"TrackersConditionsDataset",
"Transformer",
"TransformerLossModelTypes",
"TransformerSpecification",
Expand Down
14 changes: 14 additions & 0 deletions solarfarmer/models/_proto/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated protobuf message classes for SolarFarmer models.
from solarfarmer.models._proto.trackers_conditions_dataset_pb2 import (
LongTuple,
NullableShortWrapper,
ShortArrayWrapper,
TrackersConditionsDatasetDto,
)

__all__ = [
"LongTuple",
"NullableShortWrapper",
"ShortArrayWrapper",
"TrackersConditionsDatasetDto",
]
38 changes: 38 additions & 0 deletions solarfarmer/models/_proto/trackers_conditions_dataset.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Proto3 schema for TrackersConditionsDataset wire format.
// Mirrors the C# protobuf-net DTO (TrackersConditionsDatasetDto) used by
// the SolarFarmer API to store tracker condition data in binary form.
//
// Encoding notes:
// • NullableShortWrapper uses proto3 optional so that value=0 can be
// distinguished from a null (absent) entry, matching the C# short? type.
// • DateTimeOffset is encoded as a pair of .NET ticks: item1 = local
// DateTime ticks, item2 = UTC-offset ticks (100 ns intervals since
// 0001-01-01T00:00:00).
//
// Source of truth for the serialized descriptor embedded in _pb2.py.

syntax = "proto3";

message ShortArrayWrapper {
repeated int32 values = 1;
}

message NullableShortWrapper {
optional int32 value = 1;
}

message LongTuple {
int64 item1 = 1;
int64 item2 = 2;
}

message TrackersConditionsDatasetDto {
double offset_from_utc = 1;
bool rotations_are_at_middle_of_period = 2;
repeated string tracker_rotation_ids = 3;
optional double period_in_minutes_for_all_records = 4;
repeated LongTuple start_of_period = 5;
repeated float period_in_minutes = 6;
repeated ShortArrayWrapper tracker_rotations_array_values = 7;
repeated NullableShortWrapper tracker_rotation_unique_value = 8;
}
39 changes: 39 additions & 0 deletions solarfarmer/models/_proto/trackers_conditions_dataset_pb2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Auto-generated from trackers_conditions_dataset.proto — do not edit by hand.
# Regenerate with:
# python -m grpc_tools.protoc -I solarfarmer/models/_proto \
# --python_out=solarfarmer/models/_proto \
# trackers_conditions_dataset.proto
#
# The serialized FileDescriptorProto bytes below are equivalent to running
# protoc on the accompanying .proto source file.

from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder

_sym_db = _symbol_database.Default()

DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
b"\n;solarfarmer/models/_proto/trackers_conditions_dataset.proto"
b'"#\n\x11ShortArrayWrapper\x12\x0e\n\x06values\x18\x01 \x03(\x05'
b'"4\n\x14NullableShortWrapper\x12\x12\n\x05value\x18\x01 \x01(\x05'
b"H\x00\x88\x01\x01B\x08\n\x06_value"
b'")\n\tLongTuple\x12\r\n\x05item1\x18\x01 \x01(\x03\x12\r\n\x05item2\x18\x02 \x01(\x03'
b'"\x8d\x03\n\x1cTrackersConditionsDatasetDto'
b"\x12\x17\n\x0foffset_from_utc\x18\x01 \x01(\x01"
b"\x12)\n!rotations_are_at_middle_of_period\x18\x02 \x01(\x08"
b"\x12\x1c\n\x14tracker_rotation_ids\x18\x03 \x03(\t"
b"\x12.\n!period_in_minutes_for_all_records\x18\x04 \x01(\x01H\x00\x88\x01\x01"
b'\x12"\n\x0fstart_of_period\x18\x05 \x03(\x0b2\tLongTuple'
b"\x12\x19\n\x11period_in_minutes\x18\x06 \x03(\x02"
b"\x129\n\x1etracker_rotations_array_values\x18\x07 \x03(\x0b2\x11ShortArrayWrapper"
b"\x12;\n\x1dtracker_rotation_unique_value\x18\x08 \x03(\x0b2\x14NullableShortWrapper"
b'B$\n"_period_in_minutes_for_all_records'
b"b\x06proto3"
)

_builder.BuildTopDescriptorsAndMessages(
DESCRIPTOR,
"solarfarmer/models/_proto/trackers_conditions_dataset.proto",
globals(),
)
5 changes: 5 additions & 0 deletions solarfarmer/models/energy_calculation_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .ond_supplements import OndFileSupplements
from .pan_supplements import PanFileSupplements
from .pv_plant import PVPlant
from .trackers_conditions_dataset import TrackersConditionsDataset


class EnergyCalculationInputs(SolarFarmerBaseModel):
Expand All @@ -34,6 +35,9 @@ class EnergyCalculationInputs(SolarFarmerBaseModel):
PAN file overrides keyed by module spec ID
ond_file_supplements : dict[str, OndFileSupplements] or None
OND file overrides keyed by inverter spec ID
trackers_conditions_dataset : TrackersConditionsDataset or None
Custom tracker rotation schedules. Required when any layout uses
``TrackerAlgorithm.CUSTOM_ROTATIONS``.
"""

location: Location
Expand All @@ -44,6 +48,7 @@ class EnergyCalculationInputs(SolarFarmerBaseModel):
horizon_angles: list[float] | None = None
pan_file_supplements: dict[str, PanFileSupplements] | None = None
ond_file_supplements: dict[str, OndFileSupplements] | None = None
trackers_conditions_dataset: TrackersConditionsDataset | None = None


class EnergyCalculationInputsWithFiles(SolarFarmerBaseModel):
Expand Down
11 changes: 11 additions & 0 deletions solarfarmer/models/energy_calculation_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class EnergyCalculationOptions(SolarFarmerBaseModel):
calculations with incorrect results.
default_wind_speed : float
Wind speed (m/s) when met data has no wind
custom_tracker_rotations_are_at_middle_of_period : bool or None
When using a custom rotation table, specifies whether the supplied
rotation values represent the middle of each time-step (``True``) or
the start (``False``). If ``None``, the engine uses its own default.
calculate_dhi : bool
Whether to calculate diffuse horizontal irradiance (DHI) from global
horizontal irradiance (GHI) when the ``DHI`` column is missing from the
Expand Down Expand Up @@ -104,6 +108,10 @@ class EnergyCalculationOptions(SolarFarmerBaseModel):
Return detailed time-series results
return_loss_tree_time_series_results : bool
Return loss-tree time-series results
return_tracker_rotations_time_series : bool
Return tracker rotation angles as a time-series output. Default is False
return_tracker_incidence_angles_time_series : bool
Return tracker incidence angles as a time-series output. Default is False
desired_variables_for_pv_syst_format_time_series : list[str] or None
Specific variables to include in PVsyst-format output
choice_columns_order_pv_syst_format_time_series : OrderColumnsPvSystFormatTimeSeries or None
Expand Down Expand Up @@ -146,6 +154,7 @@ class EnergyCalculationOptions(SolarFarmerBaseModel):
# --- General options ---
calculation_year: int = 1990
default_wind_speed: float = 0.0
custom_tracker_rotations_are_at_middle_of_period: bool | None = None
calculate_dhi: bool = Field(False, alias="calculateDHI")

# --- Horizon options ---
Expand Down Expand Up @@ -188,6 +197,8 @@ class EnergyCalculationOptions(SolarFarmerBaseModel):
return_pv_syst_format_time_series_results: bool = True
return_detailed_time_series_results: bool = False
return_loss_tree_time_series_results: bool = False
return_tracker_rotations_time_series: bool = False
return_tracker_incidence_angles_time_series: bool = False
desired_variables_for_pv_syst_format_time_series: list[str] | None = Field(default_factory=list)
choice_columns_order_pv_syst_format_time_series: OrderColumnsPvSystFormatTimeSeries | None = (
None
Expand Down
Loading
Loading