From a1b1f7fd949b9cdd1ccf5ec63cf427f42f669a78 Mon Sep 17 00:00:00 2001 From: zhsain Date: Thu, 11 Sep 2025 17:10:46 -0400 Subject: [PATCH 1/7] Upload new example notebook --- examples/Worldwide_hopp_notebook_v3.ipynb | 602 ++++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 examples/Worldwide_hopp_notebook_v3.ipynb diff --git a/examples/Worldwide_hopp_notebook_v3.ipynb b/examples/Worldwide_hopp_notebook_v3.ipynb new file mode 100644 index 000000000..7476f971d --- /dev/null +++ b/examples/Worldwide_hopp_notebook_v3.ipynb @@ -0,0 +1,602 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c5b2637c", + "metadata": {}, + "source": [ + "## Worldwide multi-location HOPP example\n", + "---\n", + "This example shows how to simulate a hybrid renewable energy plant using HOPP for any one (onshore) location or set of locations in the world.\n", + "\n", + "We download solar resource data from NSRDB (through NREL API) and wind resource data from the Open-Meteo database. \n", + "\n", + "Unlike the NREL Wind Toolkit database, the Open-Meteo database provides worldwide coverage, though the resulting data require some modification to be usable in HOPP." + ] + }, + { + "cell_type": "markdown", + "id": "ea3793e9", + "metadata": {}, + "source": [ + "### Import required modules\n", + "We start by importing the necessary HOPP modules and other packages that will be needed later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51129d62", + "metadata": {}, + "outputs": [], + "source": [ + "# Import HOPP modules\n", + "from hopp.simulation.hopp_interface import HoppInterface\n", + "from hopp.tools.dispatch.plot_tools import plot_battery_output, plot_generation_profile\n", + "# Import other packages\n", + "import pandas as pd\n", + "import numpy as np\n", + "import os\n", + "import datetime\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "id": "f9190a9a", + "metadata": {}, + "source": [ + "### Set NREL API key\n", + "To access the NSRDB (solar resource) data, we need to set an API key. You can obtain an API key from the [NREL developer website](https://developer.nrel.gov/signup/).\n", + "\n", + "**IMPORTANT!** Enter below your own API key and associated email address" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "635bfe32", + "metadata": {}, + "outputs": [], + "source": [ + "api_key = 'your-api-key'\n", + "email_address = 'your-email-address'" + ] + }, + { + "cell_type": "markdown", + "id": "e014d32a", + "metadata": {}, + "source": [ + "### Define the supporting functions" + ] + }, + { + "cell_type": "markdown", + "id": "48182af5", + "metadata": {}, + "source": [ + "(1) A function that uses the NREL API to request NSRDB (solar resource) data for a set of user-specified coordinates and saves it as a CSV file in a user-specified directory." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0c855f5f", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# Function to get solar data via NREL API for one location\n", + "# lat = latitude, lon = longitude, name = location name (e.g. city, village)\n", + "def nrel_query(lat, lon, name, solar_output_dir):\n", + " \n", + " # URL for the NREL Meteosat Prime Meridian TMY dataset API\n", + " NSRDB_URL = f\"https://developer.nrel.gov/api/nsrdb/v2/solar/nsrdb-msg-v1-0-0-tmy-download.csv?api_key={api_key}&wkt=POINT({lat} {lon})&attributes=ghi,dhi,dni,wind_speed,air_temperature,solar_zenith_angle,surface_pressure,dew_point&names=tmy-2022&utc=false&leap_day=false&interval=60&email={email_address}\"\n", + " \n", + " # Send request to the NREL API and save response\n", + " print(f\"\\nSending request to NREL API for location with lat {lat} and lon {lon}...\")\n", + " response = requests.get(NSRDB_URL)\n", + "\n", + " # Raise an HTTPError for bad responses (4XX or 5XX client/server errors)\n", + " response.raise_for_status()\n", + " print(f\"Received HTTP {response.status_code} response from API.\")\n", + "\n", + " response_text = response.text\n", + "\n", + " # Check for API-specific errors within the CSV content even if status is 200\n", + " first_few_lines = response_text.splitlines()\n", + " is_api_error = False\n", + " if first_few_lines:\n", + " # Check if the first line (potential header of an error CSV) contains \"error message\"\n", + " if \"error message\" in first_few_lines[0].lower():\n", + " is_api_error = True\n", + "\n", + " if is_api_error:\n", + " print(f\"API Error: Received an error message from NREL API within the data response:\")\n", + " print(\"-------------------- API ERROR RESPONSE (first 5 lines) --------------------\")\n", + " print(\"\\n\".join(first_few_lines[:5])) # Print first 5 lines of the error\n", + " print(\"--------------------------------------------------------------------------\")\n", + "\n", + " # Set up output directory\n", + " os.makedirs(solar_output_dir, exist_ok=True)\n", + "\n", + " # Define output filename\n", + " filename = f\"{lat}_{lon}_{name}_NSRDB.csv\"\n", + " filepath = os.path.join(solar_output_dir, filename)\n", + "\n", + " # Save the data\n", + " print(f\"INFO: Saving data to {filepath}...\")\n", + " with open(filepath, 'w', newline='', encoding='utf-8') as f:\n", + " f.write(response_text)\n", + "\n", + " print(f\"SUCCESS! Downloaded and saved solar data to: {filepath}\")\n", + "\n", + " # Delay for 1 second to prevent API rate limiting\n", + " time.sleep(1)\n", + "\n", + " return filepath" + ] + }, + { + "cell_type": "markdown", + "id": "afb96a08", + "metadata": {}, + "source": [ + "(2) A function that queries the Open-Meteo database for wind data for a set of user-specified coordinates and saves it as a CSV file in a user-specified directory." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5c1f116a", + "metadata": {}, + "outputs": [], + "source": [ + "import openmeteo_requests\n", + "import requests_cache\n", + "from retry_requests import retry\n", + "\n", + "# Function to get wind data via OpenMeteo API for one location\n", + "# lat = latitude, lon = longitude, name = location name (e.g. city, village)\n", + "def openmeteo_query(lat, lon, name, wind_output_dir):\n", + " \n", + " # Set up the OpenMeteo API client with cache and retry on error\n", + " cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)\n", + " retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)\n", + " openmeteo = openmeteo_requests.Client(session = retry_session)\n", + "\n", + " # Make sure all required weather variables are listed here (modify as needed)\n", + " # The order of variables in hourly or daily is important to assign them correctly below\n", + " url = \"https://historical-forecast-api.open-meteo.com/v1/forecast\"\n", + " params = {\n", + " \"latitude\": lat, \n", + " \"longitude\": lon,\n", + " \"start_date\": \"2022-01-01\",\n", + " \"end_date\": \"2022-12-31\",\n", + " \"hourly\": [\"temperature_80m\", \"wind_speed_80m\", \"wind_direction_80m\", \"temperature_120m\", \"wind_speed_120m\", \"wind_direction_120m\"],\n", + " \"timezone\": \"auto\",\n", + " \"wind_speed_unit\": \"ms\"\n", + " }\n", + " # Open-Meteo API query\n", + " print(f\"\\nSending request to Open-Meteo API for location with lat {lat} and lon {lon}...\")\n", + " responses = openmeteo.weather_api(url, params=params)\n", + "\n", + " # Output directory setup\n", + " os.makedirs(wind_output_dir, exist_ok=True)\n", + "\n", + " # Print data from API query\n", + " response = responses[0]\n", + " print(f\"Coordinates {response.Latitude()}°N {response.Longitude()}°E\")\n", + " print(f\"Elevation {response.Elevation()} m asl\")\n", + " print(f\"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}\")\n", + " print(f\"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s\")\n", + "\n", + " # Process hourly data. The order of variables needs to be the same as requested.\n", + " hourly = response.Hourly()\n", + " hourly_temperature_80m = hourly.Variables(0).ValuesAsNumpy()\n", + " hourly_wind_speed_80m = hourly.Variables(1).ValuesAsNumpy()\n", + " hourly_wind_direction_80m = hourly.Variables(2).ValuesAsNumpy()\n", + " hourly_wind_speed_120m = hourly.Variables(3).ValuesAsNumpy()\n", + " hourly_wind_direction_120m = hourly.Variables(4).ValuesAsNumpy()\n", + " hourly_temperature_120m = hourly.Variables(5).ValuesAsNumpy()\n", + "\n", + " hourly_data = {\"date\": pd.date_range(\n", + " start = pd.to_datetime(hourly.Time(), unit = \"s\", utc = True),\n", + " end = pd.to_datetime(hourly.TimeEnd(), unit = \"s\", utc = True),\n", + " freq = pd.Timedelta(seconds = hourly.Interval()),\n", + " inclusive = \"left\"\n", + " )}\n", + "\n", + " hourly_data[\"temperature_80m\"] = hourly_temperature_80m\n", + " hourly_data[\"wind_speed_80m\"] = hourly_wind_speed_80m\n", + " hourly_data[\"wind_direction_80m\"] = hourly_wind_direction_80m\n", + " hourly_data[\"wind_speed_120m\"] = hourly_wind_speed_120m\n", + " hourly_data[\"wind_direction_120m\"] = hourly_wind_direction_120m\n", + " hourly_data[\"temperature_120m\"] = hourly_temperature_120m\n", + "\n", + " hourly_dataframe = pd.DataFrame(data = hourly_data)\n", + " print(hourly_dataframe.head())\n", + " printable_data = hourly_dataframe.to_string()\n", + "\n", + " # Define output filename\n", + " filename = f\"{response.Latitude()}_{response.Longitude()}_{response.Elevation()}_{name}_openmeteo_wind.csv\"\n", + " filepath = os.path.join(wind_output_dir, filename)\n", + "\n", + " # Save the data\n", + " print(f\"INFO: Saving data to {filepath}...\")\n", + " with open(filepath, 'w', newline='', encoding='utf-8') as f:\n", + " f.write(printable_data)\n", + "\n", + " print(f\"SUCCESS! Downloaded and saved wind data to: {filepath}\")\n", + "\n", + " # Delay for 1 second to prevent API rate limiting\n", + " time.sleep(1)\n", + "\n", + " return filepath" + ] + }, + { + "cell_type": "markdown", + "id": "54304ea9", + "metadata": {}, + "source": [ + "(3) A function that converts the wind data obtained through Open-Meteo to a HOPP-friendly format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8623e244", + "metadata": {}, + "outputs": [], + "source": [ + "# Function converts a OpenMeteo-formatted CSV file to a WTK-formatted SRW file.\n", + "def convert_to_srw(input_filepath, output_dir):\n", + " \n", + " try:\n", + " # --- 1. Data Import and Initial Manipulation ---\n", + " print(\"\\nConverting OpenMeteo-formatted CSV file to WTK-formatted SRW file...\")\n", + " print(f\"Processing file: {input_filepath}\")\n", + " df = pd.read_csv(input_filepath, sep='\\s+')\n", + "\n", + " # --- 2. Column Modification ---\n", + " # Reordering the columns (if needed).\n", + " df = df[[\n", + " 'date', 'temperature_80m', 'wind_speed_80m','wind_direction_80m', \n", + " 'wind_speed_120m', 'wind_direction_120m', 'temperature_120m'\n", + " ]]\n", + " # Delete the 'date' column.\n", + " df = df.drop(columns=['date'])\n", + " # Insert a column of zeros at position 2\n", + " df.insert(1, 'pressure_80m', 1.0)\n", + " # Insert another column of zeros at position 6\n", + " df.insert(5, 'pressure_120m', 1.0)\n", + " # Rounding to 3 decimals\n", + " df = df.round(4)\n", + " # Convert to numpy array\n", + " data = df.to_numpy()\n", + "\n", + " # --- 3. File Output ---\n", + " # Create the full path for the new .srw file.\n", + " base_filename = os.path.basename(input_filepath)\n", + " filename_without_ext = os.path.splitext(base_filename)[0]\n", + " split_filename = filename_without_ext.split(\"_\")\n", + " latitude = round(float(split_filename[0]), 4)\n", + " longitude = round(float(split_filename[1]), 4)\n", + " altitude = round(float(split_filename[2]), 1)\n", + " name = str(split_filename[3])\n", + " output_filepath = os.path.join(output_dir, f\"{latitude}_{longitude}_{altitude}_{name}_openmeteo_wind_converted.srw\")\n", + "\n", + " # Define the 5-line header similar to the wtk_file format.\n", + " header_lines = [\n", + " f\"123456,{name},state??,Country,2022,{latitude},{longitude},Not Available,1,8760\\n\",\n", + " \"WIND Toolkit data converted from OpenMeteo data\\n\",\n", + " \"Temperature,Pressure,Speed,Direction,Temperature,Pressure,Speed,Direction\\n\",\n", + " \"C,atm,m/s,Degrees,C,atm,m/s,Degrees\\n\",\n", + " \"80,80,80,80,120,120,120,120\\n\"\n", + " ]\n", + "\n", + " # Write the header and the modified data to the new file.\n", + " with open(output_filepath, 'w') as fi:\n", + " # Write the custom 5-line header\n", + " fi.writelines(header_lines)\n", + " # Append the DataFrame to the file as comma-delimited, without its own header.\n", + " #df.to_csv(fi, index=False, header=False, sep=',')\n", + " np.savetxt(fi, data, fmt='%.4f', delimiter=',')\n", + "\n", + " print(f\"Successfully converted to: {output_filepath}\\n\")\n", + "\n", + " except FileNotFoundError:\n", + " print(f\"Error: Input file not found at {input_filepath}\\n\")\n", + "\n", + " except Exception as e:\n", + " print(f\"An error occurred while processing {input_filepath}: {e}\\n\")\n", + "\n", + " return output_filepath" + ] + }, + { + "cell_type": "markdown", + "id": "6ec96246", + "metadata": {}, + "source": [ + "(4) A function that modifies the HOPP configuration YAML file to enable sequential HOPP simulations for multiple locations." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d9c64000", + "metadata": {}, + "outputs": [], + "source": [ + "import ruamel.yaml\n", + "\n", + "def yaml_modifier(file_path, new_lat, new_lon, solar_file, wind_file):\n", + "\n", + " # YAML parser\n", + " yaml = ruamel.yaml.YAML()\n", + " yaml.preserve_quotes = True \n", + "\n", + " # Load the YAML file\n", + " with open(file_path, 'r') as file:\n", + " data = yaml.load(file)\n", + "\n", + " # Locate and modify the data\n", + " data['site']['solar_resource_file'] = solar_file\n", + " data['site']['wind_resource_file'] = wind_file\n", + " data['site']['data']['lat'] = new_lat\n", + " data['site']['data']['lon'] = new_lon\n", + "\n", + " # Write the changes back to the file\n", + " with open(file_path, 'w') as file:\n", + " yaml.dump(data, file)\n", + "\n", + " print(f\"Successfully updated '{file_path}' with data for ({new_lat},{new_lon})\")" + ] + }, + { + "cell_type": "markdown", + "id": "be3ac9cc", + "metadata": {}, + "source": [ + "(5) A function that runs the HOPP simulation for all the locations provided in an excel file. \n", + "\n", + "To run for one location, set **RANGE = [location_index]** where location_index = 0, 1, 2, ...\n", + "\n", + "The HOPP results are saved in a separate txt file for each location. A single txt file with key results from all locations is also saved at the end. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "032861c5", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def hopp_simulation(coord_file, yaml_file, solar_dir, wind_dir, hopp_dir):\n", + " '''\n", + " coord_file: Excel file containing locations and coordinates\n", + " yaml_file: Yaml file containing HOPP configuration\n", + " solar_dir: Directory containing solar data files\n", + " wind_dir: Directory containing wind data files\n", + " hopp_dir: Directory for HOPP output\n", + " '''\n", + " # Get today's date\n", + " time_now = datetime.date.today()\n", + "\n", + " # Import locations and coordinates\n", + " df = pd.read_excel(coord_file)\n", + " lat = df['latitude'].to_numpy()\n", + " lon = df['longitude'].to_numpy()\n", + " name = df['location'].to_numpy()\n", + "\n", + " # Identify locations to simulate\n", + " # For first location: RANGE = [0], for third location: RANGE = [2], or for all locations: RANGE = range(len(name))\n", + " RANGE = range(len(name))\n", + "\n", + " # Initialize arrays for saving key results\n", + " lcoe_data = np.zeros(len(RANGE))\n", + " energy_data = np.zeros(len(RANGE))\n", + " cf_data = np.zeros(len(RANGE))\n", + " k = 0 #index for lcoe array\n", + "\n", + " # Iterate for all locations specified in RANGE\n", + " for idx in RANGE:\n", + " latitude = np.round(lat[idx], 2)\n", + " longitude = np.round(lon[idx], 2)\n", + " LAT = latitude.item() #convert np.array to float\n", + " LON = longitude.item()\n", + " LOCATION = name[idx]\n", + "\n", + " # Get solar data from NREL API\n", + " SOLAR_FILE = nrel_query(LAT, LON, LOCATION, solar_dir)\n", + "\n", + " # Get wind data from OpenMeteo API\n", + " WIND_FILE = openmeteo_query(LAT, LON, LOCATION, wind_dir)\n", + "\n", + " # Convert wind data to SRW format\n", + " CONVERTED_WIND_FILE = convert_to_srw(WIND_FILE, wind_dir)\n", + "\n", + " # Modify YAML file for desired location\n", + " yaml_modifier(yaml_file, LAT, LON, SOLAR_FILE, CONVERTED_WIND_FILE)\n", + "\n", + " #HOPP simulation\n", + " hi = HoppInterface(yaml_file)\n", + " print(\"\\nHOPP simulation in progress...\")\n", + " hi.simulate(project_life=20) #specify the project lifetime in years\n", + "\n", + " hybrid_plant = hi.system\n", + " print(\"\\nPrinting and saving HOPP output...\")\n", + "\n", + " #HOPP Output\n", + " if not os.path.exists(hopp_dir):\n", + " os.makedirs(hopp_dir)\n", + " output_file = os.path.join(hopp_dir, f\"hopp_output_{LAT}_{LON}_{LOCATION}_{time_now}.txt\")\n", + "\n", + " Wind_eff = hybrid_plant.wind.value(\"annual_energy\") / hybrid_plant.wind.value(\"annual_gross_energy\")\n", + " energy = hybrid_plant.annual_energies\n", + " cf = hybrid_plant.capacity_factors\n", + " lcoe_n = hybrid_plant.lcoe_nom\n", + " lcoe_r = hybrid_plant.lcoe_real\n", + "\n", + " # Print to file\n", + " with open(output_file, \"w\") as log:\n", + " print(f\"Wind output efficiency:\\n {Wind_eff} \\nAnnual Energies (kWh):\\n {energy} \\nCapacity factors (%):\\n {cf} \\nNominal LCOE (c/kWh):\\n {lcoe_n} \\nReal LCOE (c/kWh):\\n {lcoe_r}\"\n", + " , file=log)\n", + " print(f\"HOPP output saved in '{output_file}'\\n\")\n", + "\n", + " # Save key results for this location\n", + " lcoe_data[k] = lcoe_r[\"hybrid\"]\n", + " energy_data[k] = energy[\"hybrid\"]\n", + " cf_data[k] = cf[\"hybrid\"]\n", + " k += 1\n", + " \n", + " # Print to terminal\n", + " print(\"Wind output efficieny:\", Wind_eff)\n", + " print(\"\\nAnnual Energies (kWh):\")\n", + " print(energy)\n", + " print(\"\\nCapacity factors (%):\")\n", + " print(cf)\n", + " print(\"\\nNominal LCOE (cents/kWh):\")\n", + " print(lcoe_n)\n", + " print(\"\\nInflation-adjusted LCOE (cents/kWh):\")\n", + " print(lcoe_r)\n", + "\n", + " # Plot generation and battery dispatch profiles (We advise to comment these out if running multiple locations)\n", + " #plot_generation_profile(hybrid_plant)\n", + " #plot_battery_output(hybrid_plant)\n", + "\n", + " # Save key results\n", + " data_stack = np.column_stack([name[RANGE], energy_data, cf_data, lcoe_data])\n", + " lcoe_file = os.path.join(hopp_dir, f\"LCOE_output_{time_now}.txt\")\n", + " with open(lcoe_file, \"w\") as log:\n", + " print(f\"Location, Annual generation (kWh), Capacity factor (%), Real LCOE (cents/kWh)\\n {data_stack}\", file=log)\n", + " print(f\"\\nKey HOPP results saved in '{lcoe_file}'\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "3511583e", + "metadata": {}, + "source": [ + "### Run the main code\n", + "Specify the excel file containing the locations' coordinates and names, the HOPP configuration YAML file, the directories containig solar and wind resource data files, and the output directory. To assist the user, an excel file is provided with coordinates for locations in western Africa.\n", + "\n", + "Then, run the code below to simulate a hybrid renewable energy plant in all desired locations. \n", + "\n", + "Check the results in the output directory." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9b5ddfea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Sending request to NREL API for location with lat 34.92 and lon -6.18...\n", + "Received HTTP 200 response from API.\n", + "INFO: Saving data to ./inputs/solar_data/34.92_-6.18_Larache_NSRDB.csv...\n", + "SUCCESS! Downloaded and saved solar data to: ./inputs/solar_data/34.92_-6.18_Larache_NSRDB.csv\n", + "\n", + "Sending request to Open-Meteo API for location with lat 34.92 and lon -6.18...\n", + "Coordinates 34.9375°N -6.1875°E\n", + "Elevation 85.0 m asl\n", + "Timezone b'Africa/Casablanca'b'GMT+1'\n", + "Timezone difference to GMT+0 3600 s\n", + " date temperature_80m wind_speed_80m \\\n", + "0 2021-12-31 23:00:00+00:00 18.973000 1.780449 \n", + "1 2022-01-01 00:00:00+00:00 18.622999 1.920937 \n", + "2 2022-01-01 01:00:00+00:00 18.172998 3.220248 \n", + "3 2022-01-01 02:00:00+00:00 17.823000 3.764306 \n", + "4 2022-01-01 03:00:00+00:00 17.372999 3.360060 \n", + "\n", + " wind_direction_80m wind_speed_120m wind_direction_120m temperature_120m \n", + "0 128.157272 19.172998 1.817277 128.157272 \n", + "1 141.340164 18.772999 2.041369 143.130020 \n", + "2 143.841721 18.473000 3.369805 144.865723 \n", + "3 140.389343 18.223000 4.064836 141.115494 \n", + "4 143.471054 17.823000 3.595649 145.407654 \n", + "INFO: Saving data to ./inputs/wind_data/34.9375_-6.1875_85.0_Larache_openmeteo_wind.csv...\n", + "SUCCESS! Downloaded and saved wind data to: ./inputs/wind_data/34.9375_-6.1875_85.0_Larache_openmeteo_wind.csv\n", + "\n", + "Converting OpenMeteo-formatted CSV file to WTK-formatted SRW file...\n", + "Processing file: ./inputs/wind_data/34.9375_-6.1875_85.0_Larache_openmeteo_wind.csv\n", + "Successfully converted to: ./inputs/wind_data/34.9375_-6.1875_85.0_Larache_openmeteo_wind_converted.srw\n", + "\n", + "Successfully updated './inputs/03-wind-solar-battery.yaml' with data for (34.92,-6.18)\n", + "\n", + "HOPP simulation in progress...\n", + "\n", + "Printing and saving HOPP output...\n", + "HOPP output saved in './hopp_output/hopp_output_34.92_-6.18_Larache_2025-09-11.txt'\n", + "\n", + "Wind output efficieny: 0.8273167449154486\n", + "\n", + "Annual Energies (kWh):\n", + "{\"pv\": 758505765.6473475, \"wind\": 173379028.88429186, \"battery\": 8503.958886887514, \"hybrid\": 931893298.490529}\n", + "\n", + "Capacity factors (%):\n", + "{\"pv\": 28.14091025518127, \"wind\": 10.995625880536014, \"battery\": 0, \"hybrid\": 13.505338631270558}\n", + "\n", + "Nominal LCOE (cents/kWh):\n", + "{\"pv\": 4.735263428696596, \"wind\": 13.977866407691577, \"battery\": 290685.3032543002, \"hybrid\": 9.107426314617603}\n", + "\n", + "Inflation-adjusted LCOE (cents/kWh):\n", + "{\"pv\": 3.8753528517482616, \"wind\": 11.4395250148341, \"battery\": 237897.66628420443, \"hybrid\": 7.45354320238008}\n", + "\n", + "Key HOPP results saved in './hopp_output/LCOE_output_2025-09-11.txt'\n", + "\n", + "SUCCESS! HOPP simulation completed for all locations and results saved.\n" + ] + } + ], + "source": [ + "# INPUT FILES (MODIFY FILE PATHS AS NEEDED)\n", + "# Excel file with at least three columns (latitude, longitude, and location)\n", + "COORD_FILE = './inputs/west_africa_coordinates.xlsx'\n", + "# YAML file for HOPP configuration\n", + "YAML_FILE = \"./inputs/03-wind-solar-battery.yaml\"\n", + "\n", + "# Directories for weather data and HOPP output (modify as needed)\n", + "SOLAR_DIR = './inputs/solar_data/'\n", + "WIND_DIR = './inputs/wind_data/'\n", + "HOPP_DIR = './hopp_output/'\n", + "\n", + "# Run HOPP simulation for all locations\n", + "hopp_simulation(COORD_FILE, YAML_FILE, SOLAR_DIR, WIND_DIR, HOPP_DIR)\n", + "\n", + "print(\"\\nSUCCESS! HOPP simulation completed for all locations and results saved.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "h2integrate", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From aacbba402565256d3a47aea1ba1c83e7308f58e6 Mon Sep 17 00:00:00 2001 From: zhsain Date: Thu, 11 Sep 2025 17:11:47 -0400 Subject: [PATCH 2/7] Rename Worldwide_hopp_notebook_v3.ipynb to 12-worldwide-multi-location-example.ipynb --- ...otebook_v3.ipynb => 12-worldwide-multi-location-example.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{Worldwide_hopp_notebook_v3.ipynb => 12-worldwide-multi-location-example.ipynb} (100%) diff --git a/examples/Worldwide_hopp_notebook_v3.ipynb b/examples/12-worldwide-multi-location-example.ipynb similarity index 100% rename from examples/Worldwide_hopp_notebook_v3.ipynb rename to examples/12-worldwide-multi-location-example.ipynb From d0ae6800e9e8eba71e41e01caee2f393c0d6e231 Mon Sep 17 00:00:00 2001 From: zhsain Date: Thu, 11 Sep 2025 17:12:36 -0400 Subject: [PATCH 3/7] Upload coordinates file --- examples/inputs/west_africa_coordinates.xlsx | Bin 0 -> 10223 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/inputs/west_africa_coordinates.xlsx diff --git a/examples/inputs/west_africa_coordinates.xlsx b/examples/inputs/west_africa_coordinates.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4a00e46222fe494a5a07c9045fcd4b7caabed84d GIT binary patch literal 10223 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 Date: Thu, 11 Sep 2025 17:23:54 -0400 Subject: [PATCH 4/7] Update RELEASE.md --- RELEASE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index a21404b49..15ecdbd05 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,9 @@ # Release Notes ## Unreleased +* Added excel file with coordinates for 50 locations in west Africa. This excel file is used in example 12, but can also be broadly useful in testing other HOPP scripts. + - `examples/inputs/west_africa_coordinates.xlsx` +* Added an example that allows HOPP simulations to be run sequentially for multiple locations anywhere in the world. + - `examples/12-worldwide-multi-location-example.ipynb` * Add `overwrite_fin_values` from H2Integrate to sync cost input methods * Bump minimum NREL-PySAM version to 7.0.0 * Clarify that the `nominal_discount_rate` method of the `CustomFinancialModel` uses the Fisher equation From 5348096c11137f498bef74256a35523174bf2277 Mon Sep 17 00:00:00 2001 From: zhsain Date: Thu, 13 Nov 2025 16:02:53 -0500 Subject: [PATCH 5/7] Delete examples/inputs/west_africa_coordinates.xlsx --- examples/inputs/west_africa_coordinates.xlsx | Bin 10223 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/inputs/west_africa_coordinates.xlsx diff --git a/examples/inputs/west_africa_coordinates.xlsx b/examples/inputs/west_africa_coordinates.xlsx deleted file mode 100644 index 4a00e46222fe494a5a07c9045fcd4b7caabed84d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10223 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 Date: Thu, 13 Nov 2025 16:03:38 -0500 Subject: [PATCH 6/7] Add West Africa coordinates file --- examples/inputs/west_africa_coordinates.csv | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 examples/inputs/west_africa_coordinates.csv diff --git a/examples/inputs/west_africa_coordinates.csv b/examples/inputs/west_africa_coordinates.csv new file mode 100644 index 000000000..49fd88e10 --- /dev/null +++ b/examples/inputs/west_africa_coordinates.csv @@ -0,0 +1,51 @@ +latitude,longitude,location +35.7775,-5.8000,Tangier +35.5767,-5.3667,Tétouan +35.4750,-6.0033,Asilah +34.9239,-6.1814,Larache +34.2575,-6.5800,Kénitra +34.0209,-6.8416,Rabat +34.0388,-6.7888,Salé +33.6833,-7.3833,Mohammedia +33.5731,-7.5898,Casablanca +33.2453,-8.4900,El Jadida +32.2939,-9.2333,Safi +31.5126,-9.7701,Essaouira +30.4209,-9.5981,Agadir +29.6800,-9.7700,Tiznit +28.8833,-11.0833,Sidi Ifni +28.4344,-11.1308,Tan-Tan +27.1533,-13.1922,Laayoune +23.6964,-15.9500,Dakhla +21.0167,-15.9500,Nouadhibou +18.0731,-15.9582,Nouakchott +16.5333,-15.8833,Rosso +16.0228,-16.4783,Saint-Louis +14.7167,-17.4677,Dakar +14.7833,-16.9333,Thiès +14.4000,-16.9833,M'Bour +14.1500,-16.0833,Kaolack +14.3333,-16.3833,Fatick +12.5667,-16.2667,Ziguinchor +13.4433,-16.5974,Banjul +11.8744,-15.5877,Bissau +12.2500,-14.5833,Bafatá +9.5092,-13.7122,Conakry +8.4833,-13.2333,Freetown +7.5167,-12.5000,Bonthe +6.3000,-10.8000,Monrovia +5.9167,-10.2333,Buchanan +5.3167,-3.9167,Grand-Bassam +5.3167,-4.0333,Abidjan +6.8167,-5.2833,Yamoussoukro +5.1500,-4.2000,Dabou +4.6667,-5.1167,Sassandra +4.7500,-6.7500,San-Pédro +4.9167,-4.9000,Grand-Lahou +5.5167,-3.0833,Aboisso +5.9833,1.1833,Aflao +6.1333,1.2167,Lomé +6.3667,2.4000,Cotonou +6.4833,2.6167,Porto-Novo +6.4167,2.8833,Badagry +6.5244,3.3792,Lagos From 74ef7785802ca8ee1e1cce89b31d2b1da3cb11d7 Mon Sep 17 00:00:00 2001 From: zhsain Date: Thu, 13 Nov 2025 16:09:24 -0500 Subject: [PATCH 7/7] Update HOPP example for multi-location simulation --- .../12-worldwide-multi-location-example.ipynb | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/examples/12-worldwide-multi-location-example.ipynb b/examples/12-worldwide-multi-location-example.ipynb index 7476f971d..19887dd94 100644 --- a/examples/12-worldwide-multi-location-example.ipynb +++ b/examples/12-worldwide-multi-location-example.ipynb @@ -7,11 +7,11 @@ "source": [ "## Worldwide multi-location HOPP example\n", "---\n", - "This example shows how to simulate a hybrid renewable energy plant using HOPP for any one (onshore) location or set of locations in the world.\n", + "This example shows how to simulate a hybrid renewable energy plant using HOPP for any set of (onshore) locations in the world.\n", "\n", "We download solar resource data from NSRDB (through NREL API) and wind resource data from the Open-Meteo database. \n", "\n", - "Unlike the NREL Wind Toolkit database, the Open-Meteo database provides worldwide coverage, though the resulting data require some modification to be usable in HOPP." + "Unlike the NREL Wind Toolkit database, the Open-Meteo database provides worldwide coverage, though the resulting data requires some modification to be usable in HOPP." ] }, { @@ -47,9 +47,10 @@ "metadata": {}, "source": [ "### Set NREL API key\n", - "To access the NSRDB (solar resource) data, we need to set an API key. You can obtain an API key from the [NREL developer website](https://developer.nrel.gov/signup/).\n", + "To access the NSRDB (solar resource) data, we need to set an NREL API key. You can obtain an API key from the [NREL developer website](https://developer.nrel.gov/signup/).\n", "\n", - "**IMPORTANT!** Enter below your own API key and associated email address" + "**IMPORTANT!**\n", + "To set up the NREL_API_KEY required for resource downloads, you can create an Environment Variable called NREL_API_KEY. Otherwise, you can keep the key in a new file called \".env\" in the root directory of this project. The \".env\" file must contain the line: NREL_API_KEY=key" ] }, { @@ -59,8 +60,15 @@ "metadata": {}, "outputs": [], "source": [ - "api_key = 'your-api-key'\n", - "email_address = 'your-email-address'" + "from hopp.utilities.keys import set_nrel_key_dot_env\n", + "from hopp.utilities.keys import get_developer_nrel_gov_key, get_developer_nrel_gov_email\n", + "\n", + "# Set the API key\n", + "set_nrel_key_dot_env()\n", + "\n", + "# Get API key and associated email address\n", + "email_address = get_developer_nrel_gov_email()\n", + "api_key = get_developer_nrel_gov_key()" ] }, { @@ -81,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "0c855f5f", "metadata": {}, "outputs": [], @@ -90,11 +98,15 @@ "\n", "# Function to get solar data via NREL API for one location\n", "# lat = latitude, lon = longitude, name = location name (e.g. city, village)\n", - "def nrel_query(lat, lon, name, solar_output_dir):\n", - " \n", - " # URL for the NREL Meteosat Prime Meridian TMY dataset API\n", - " NSRDB_URL = f\"https://developer.nrel.gov/api/nsrdb/v2/solar/nsrdb-msg-v1-0-0-tmy-download.csv?api_key={api_key}&wkt=POINT({lat} {lon})&attributes=ghi,dhi,dni,wind_speed,air_temperature,solar_zenith_angle,surface_pressure,dew_point&names=tmy-2022&utc=false&leap_day=false&interval=60&email={email_address}\"\n", + "def nrel_query(lat, lon, name, year, solar_output_dir):\n", " \n", + " # URL for the NREL Meteosat Prime Meridian dataset API\n", + " NSRDB_URL = f\"https://developer.nrel.gov/api/nsrdb/v2/solar/nsrdb-msg-v1-0-0-tmy-download.csv?api_key={api_key}&wkt=POINT({lat} {lon})&names={year}&utc=false&leap_day=false&interval=60&email={email_address}\"\n", + "\n", + " # Make sure 'year' variable is valid\n", + " if year not in ['tmy','tmy-2014','tmy-2022']:\n", + " raise ValueError(f\"{year} is not valid TMY year for solar resource API. Please set year as either 'tmy','tmy-2014', or 'tmy-2022'\")\n", + "\n", " # Send request to the NREL API and save response\n", " print(f\"\\nSending request to NREL API for location with lat {lat} and lon {lon}...\")\n", " response = requests.get(NSRDB_URL)\n", @@ -149,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "5c1f116a", "metadata": {}, "outputs": [], @@ -160,7 +172,7 @@ "\n", "# Function to get wind data via OpenMeteo API for one location\n", "# lat = latitude, lon = longitude, name = location name (e.g. city, village)\n", - "def openmeteo_query(lat, lon, name, wind_output_dir):\n", + "def openmeteo_query(lat, lon, name, year, wind_output_dir):\n", " \n", " # Set up the OpenMeteo API client with cache and retry on error\n", " cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)\n", @@ -170,11 +182,13 @@ " # Make sure all required weather variables are listed here (modify as needed)\n", " # The order of variables in hourly or daily is important to assign them correctly below\n", " url = \"https://historical-forecast-api.open-meteo.com/v1/forecast\"\n", + " date1 = year + \"-01-01\"\n", + " date2 = year + \"-12-31\"\n", " params = {\n", " \"latitude\": lat, \n", " \"longitude\": lon,\n", - " \"start_date\": \"2022-01-01\",\n", - " \"end_date\": \"2022-12-31\",\n", + " \"start_date\": date1,\n", + " \"end_date\": date2,\n", " \"hourly\": [\"temperature_80m\", \"wind_speed_80m\", \"wind_direction_80m\", \"temperature_120m\", \"wind_speed_120m\", \"wind_direction_120m\"],\n", " \"timezone\": \"auto\",\n", " \"wind_speed_unit\": \"ms\"\n", @@ -377,7 +391,7 @@ "outputs": [], "source": [ "\n", - "def hopp_simulation(coord_file, yaml_file, solar_dir, wind_dir, hopp_dir):\n", + "def hopp_simulation(solar_year, wind_year, coord_file, yaml_file, solar_dir, wind_dir, hopp_dir):\n", " '''\n", " coord_file: Excel file containing locations and coordinates\n", " yaml_file: Yaml file containing HOPP configuration\n", @@ -389,7 +403,7 @@ " time_now = datetime.date.today()\n", "\n", " # Import locations and coordinates\n", - " df = pd.read_excel(coord_file)\n", + " df = pd.read_csv(coord_file)\n", " lat = df['latitude'].to_numpy()\n", " lon = df['longitude'].to_numpy()\n", " name = df['location'].to_numpy()\n", @@ -411,12 +425,14 @@ " LAT = latitude.item() #convert np.array to float\n", " LON = longitude.item()\n", " LOCATION = name[idx]\n", + " S_YEAR = solar_year \n", + " W_YEAR = wind_year\n", "\n", " # Get solar data from NREL API\n", - " SOLAR_FILE = nrel_query(LAT, LON, LOCATION, solar_dir)\n", + " SOLAR_FILE = nrel_query(LAT, LON, LOCATION, S_YEAR, solar_dir)\n", "\n", " # Get wind data from OpenMeteo API\n", - " WIND_FILE = openmeteo_query(LAT, LON, LOCATION, wind_dir)\n", + " WIND_FILE = openmeteo_query(LAT, LON, LOCATION, W_YEAR, wind_dir)\n", "\n", " # Convert wind data to SRW format\n", " CONVERTED_WIND_FILE = convert_to_srw(WIND_FILE, wind_dir)\n", @@ -493,7 +509,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "9b5ddfea", "metadata": {}, "outputs": [ @@ -560,9 +576,13 @@ } ], "source": [ + "# Weather data years\n", + "SOLAR_YEAR = 'tmy-2022' #either 'tmy','tmy-2014', or 'tmy-2022'\n", + "WIND_YEAR = '2022' #any year from '1940' onwards, but more recent years are strongly recommended\n", + "\n", "# INPUT FILES (MODIFY FILE PATHS AS NEEDED)\n", "# Excel file with at least three columns (latitude, longitude, and location)\n", - "COORD_FILE = './inputs/west_africa_coordinates.xlsx'\n", + "COORD_FILE = './inputs/west_africa_coordinates.csv'\n", "# YAML file for HOPP configuration\n", "YAML_FILE = \"./inputs/03-wind-solar-battery.yaml\"\n", "\n", @@ -572,7 +592,7 @@ "HOPP_DIR = './hopp_output/'\n", "\n", "# Run HOPP simulation for all locations\n", - "hopp_simulation(COORD_FILE, YAML_FILE, SOLAR_DIR, WIND_DIR, HOPP_DIR)\n", + "hopp_simulation(SOLAR_YEAR, WIND_YEAR, COORD_FILE, YAML_FILE, SOLAR_DIR, WIND_DIR, HOPP_DIR)\n", "\n", "print(\"\\nSUCCESS! HOPP simulation completed for all locations and results saved.\")" ]