Usage

Building Models with the generation_models schema

The models used by the Tyba Python Client come from the generation_models package, which is installed as a dependency of the client (it’s always a good idea to make sure you have the latest version). The package provides high-level classes for representing PV solar power plants (PVGenerationModel), grid-connected energy storage assets (StandaloneStorageModel), and hybrid systems (PVStorageModel). Each instance of these classes contains all of the information needed to model a renewable asset over some period of time, and can be passed to the client to schedule a model run and generate results.

The models have a nested class structure, so each attribute of the class is either a direct input or an object that contains inputs to some submodel. So a PVStorageModel instance might look like

from generation_models import *

model = PVStorageModel(
    energy_prices=DARTPrices(...),
    time_interval_mins=30,
    storage_inputs=MultiStorageInputs(
        batteries=[
            BatteryParams(
                ...,
                capacity_degradation_model=TableCapDegradationModel(...)
            )
        ]
    ),
    pv_inputs=PVGenerationModel(
        solar_resource=SolarResource(...),
        inverter=Inverter(...),
        pv_module=PVModule(...),
        system_design=PVSystemDesign(
            ...,
            tracking=SingleAxisTracking(...)
        ),
    ),
    storage_coupling=StorageCoupling.dc,
)

where ... represents model inputs that aren’t nested classes. Submodels are also modular, so different models (with different inputs) can be used interchangeably. For example, the Tyba Client can model inverters using the OND model (via the ONDInverter class) or CEC Model (via the Inverter class) and instances of either class can be used anywhere an inverter attribute is required. Where applicable, “convenience” inputs are provided to streamline the modeling process, e.g. we can set PVGenerationModel.solar_resource to a SolarResource instance, or to a tuple of (latitude, longitude). In the latter case, the Tyba API automatically queries the NSRDB for the relevant solar resource information so we don’t have to provide it.

Detailed information on all possible models and their inputs can be found on the Model Schema page of the generation_models documentation, but a good place to start is the docs for the 3 high level classes: PVGenerationModel, StandaloneStorageModel, and PVStorageModel.

Importing Data from Local Files

We often need to incorporate data from files (e.g. solar resource, prices, equipment parameters) into our models. While for something like price data we might use pandas, the tyba_client.io module provides functions for handling OND and PAN files and PSM-formatted solar resource CSVs. For example,

from generation_models import *
from tyba_client.io import solar_resource_from_psm_csv

solar_resource = solar_resource_from_psm_csv("my_weather.csv", typical=True)
model = PVGenerationModel(
    solar_resource=solar_resource,
    ...
)

Scheduling and Receiving Responses

Because of the computational intensity of model runs, Tyba’s API follows a “Schedule & Receive” process.

Scheduling a Model Run

Once we’ve successfully built our model, we can use the client.schedule method to send the model to the Tyba servers and request that it be scheduled for completion.

resp = client.schedule(model)

We can assess how our request went by inspecting the requests.Response object that is returned by the schedule method.

resp.raise_for_status()  # will raise an error if we got a bad response status code
print(resp.json())  # resp.json() is the response message as a dict

#returns
{"id":"d08b1e8e-6c68-40cf-b29c-0b6da18cd777"}

id_ = resp.json()["id"]  # we'll use this id to check the run's status

If our request was successful, the response message will contain an id for the scheduled run. If not, the raise_for_status method will raise an error and the response message will contain the particular HTTP status code and more information on the issue.

Receiving and Analyzing Results

To receive the simulation results, we pass the scheduled run’s id to the client.wait_on_result method. This method checks the status of the model run at some interval (customizable by the wait_time argument) and returns a results dictionary once the run is complete.

results = client.wait_on_result(id_)

Depending on the simulation requested, it might take quite a while for this line of code to execute

The structure of the results dict will depend on the model that was run, so it’s a good idea to check the keys. For example, for a PVStorageModel:

print(results.keys())
# returns
# dict_keys(['solar_only', 'solar_storage', 'solar_storage_waterfall', 'optimizer_outputs'])

Below the top level, most of the results are time series that can be converted into pandas DataFrames for in-depth analysis, plotting and easy export. Continuing the example above:

import pandas as pd
df = pd.DataFrame(results["solar_storage"])  # with the dataframe we can analyze, summarize, plot etc
# we can also export our results
df.to_csv("my-results.csv")

Receiving Results in the v1 Format

In order to make results data more consistent across different model types, Tyba has introduced a “v1” results format that organizes time series outputs according to their corresponding equipment in a “single line diagram” sense. This format will eventually become the standard for API results, but currently it is offered as an option and accessible by using the client.wait_on_result_v1 method in place of the client.wait_on_result method used in the above example.

Checking the Status of a Scheduled Run

After a simulation run has been scheduled, instead of using client.wait_on_result, we can check on the status ourselves by passing the scheduled run’s id to the client.get_status method. Similar to client.schedule, a requests.Response object is returned and the response message will contain a "status" field that we can inspect:

status = client.get_status(id_).json()["status"]
print(status)

Until a run is complete, it’s status will be "scheduled", and we must keep checking until the status has changed to "complete". To facilitate this, we can run our status check in a loop:

complete = False
while not complete:
    res = client.get_status(id).json()
    if res["status"] == "complete":
        complete = True
        print(res.keys())
    else:
        time.sleep(2)

This is, in fact, what client.wait_on_result accomplishes for us. Our status check can also return an "error" status. This means an error has occurred during the model run itself. Similar to scheduling, we can inspect res.json() to get more insight into what is going on, but it may not always be clear. Tyba is alerted when run errors occur and will typically reach out to resolve the issue. Of course, please reach out for any time-sensitive issues. A table of Tyba Model Run Status Codes with summaries is provided below.

Performing Project Finance Calculations

Once we have our results, the project_finance package (which is installed as a dependency of the Tyba Client) can be used to perform simple NPV and IRR calculations:

import project_finance as pf

res = res["result"]
revenue = pd.Series(
    data=np.array(res["market_awards"]["rtm_base_point"]) * rates.rtm + np.array(res["market_awards"]["dam_base_point"]) * rates.dam,
    index=pd.period_range(start="1990-01-01T00:00", periods=len(rates.rtm), freq="H"))
monthly_revenue = revenue.groupby(revenue.index.month).sum().values
inputs = pf.HybridFinanceInputsSingleRev(
    frequency="monthly",
    revenue=monthly_revenue,
    solar=pf.SolarInputs(
        array_capacity_wdc=model.pv_inputs.system_design.dc_capacity * 10**3, # W
        capex_cost_per_wdc=0.60,
        opex_cost_per_wdc=0.007
    ),
    storage=pf.StorageInputs(
        capacity_kwh=model.storage_inputs[0].energy_capacity,
        capex_cost_per_kwh=150.00,
        opex_cost_per_kwh=5.00
    )
)
npv = pf.npv(inputs, annual_discount_rate=0.05)
irr = pf.irr(inputs)
print(f"5% NPV: {npv}")
print(f"IRR: {irr}")

Rates Limits

To protect Tyba’s servers from being overwhelmed (such that they degrade service for other users), we impose rate limits on API requests. We monitor performance closely and seek to deliver as fast and reliable computing as we can. These rate limits will never be used to limit reasonable requests, only to protect against runaway or excessive requests. Below we provide an example of designing your code around Tyba’s rate limits, but please reach out to us if you would like additional assistance in that area.

Model Units

The units for the Tyba rate limit is a single design-year. For example, a single-year PV run would be one unit, whereas a 10-year run would be 10 units.

Limit by Model Type

Subject to change

For PV-Only runs

  • Limit is 5 units per second

For PV+Storage or Standalone Storage runs

  • Limit is 10 units per minute

  • One unit typically takes 10-15 seconds to run, so this should not be prohibitive to batched runs.

Example Rate Limit Backoff

def is_ratelimited(response):
    return response.status_code==429

def is_unprocessable(response):
    return response.status_code==422

def portfolio_schedule(models,save_name):
    ids = {}
    for project, model in models.items():
        print(f"{project} scheduling")
        tries = 8
        backoff = 2
        init = 2
        while tries > 0:
            res = client.schedule_pv_storage(model)
            if is_unprocessable(res):
                print("ERROR: could not schedule")
                print(res.text)
                raise Exception(f"could not process {project}")
            if is_ratelimited(res):
                print("Call ratelimited...retrying")
                time.sleep(init)
                init *= backoff
                tries -= 1
            else:
                print(res.text) #this will show the id for that run
                run_id = res.json()["id"]
                ids[project] = run_id
                break
        else:
            print(f"{project} exceeded tries and has failed.")
    return ids

HTTP Response Codes

HTTP Status Code

Summary

200 - Ok

Request worked as expected.

400 - Bad Request

The request was not accepted. Usually due to a missing required parameter or an incorrect parameter type.

401 - Unauthorized

An invalid or missing TYBA_PAT provided.

402 - Request Failed

The request failed despite valid parameters. Check to make sure input parameters are the appropriate scale, and would not lead to an infeasible solution. If problem persists, contact Tyba.

403 - Forbidden

The TYBA_PAT does not have sufficient permissions to perform this request.

404 - Not Found

The requested endpoint does not exist. Check your request.

422 - Unprocessable Entity

The request was accepted but was unable to process the instructions. For example, when requesting pricing data, if a pricing year is requested but not in the database, this error will be raised.

429 - Rate Limited

Too many requests were scheduled with the API too quickly. Please implement an exponential backoff of your requests. For more information on rate limits, visit this page.

500-504 - Tyba Server Error

An error occurred with Tyba’s servers. Please try refreshing and contact Tyba if the issue persists.

Tyba Model Run Status Codes

Status Code

Summary

scheduled

The request has been sent to Tyba’s servers and the model is running.

completed

The request has finished.

unknown

The requested run id is not currently scheduled. Check your run id and try again.

error

The request has failed. Try refreshing, and if issue persists, contact Tyba.