コード例 #1
0
ファイル: reopt.py プロジェクト: NREL/HOPP
    def get_run_uuid(post, API_KEY, api_url):
        """
        Function for posting job
        :param post:
        :param API_KEY:
        :param api_url:
        :return: job run_uuid
        """
        post_url = api_url + '?api_key=' + API_KEY
        resp = requests.post(post_url, json=post)
        run_id = None
        if not resp.ok:
            logger.error("Status code {}. {}".format(resp.status_code, resp.content))
        else:
            logger.info("Response OK from {}.".format(post_url))

            run_id_dict = json.loads(resp.text)

            try:
                run_id = run_id_dict['run_uuid']
            except KeyError:
                msg = "Response from {} did not contain run_uuid.".format(post_url)
                logger.error(msg)

        return run_id
コード例 #2
0
    def simulate(self, project_life: int = 25):
        """
        Run the system and financial model
        """
        if not self.system_model:
            return
        self.system_model.execute(0)

        if not self.financial_model:
            return

        self.financial_model.value("construction_financing_cost",
                                   self.get_construction_financing_cost())

        self.financial_model.Revenue.ppa_soln_mode = 1

        self.financial_model.Lifetime.system_use_lifetime_output = 1
        self.financial_model.FinancialParameters.analysis_period = project_life
        single_year_gen = self.financial_model.SystemOutput.gen
        self.financial_model.SystemOutput.gen = list(
            single_year_gen) * project_life

        if self.name != "Grid":
            self.financial_model.SystemOutput.system_pre_curtailment_kwac = self.system_model.Outputs.gen * project_life
            self.financial_model.SystemOutput.annual_energy_pre_curtailment_ac = self.system_model.Outputs.annual_energy

        self.financial_model.execute(0)
        logger.info("{} simulation executed".format(self.name))
コード例 #3
0
ファイル: reopt.py プロジェクト: NREL/HOPP
    def __init__(self, lat, lon,
                 interconnection_limit_kw: float,
                 load_profile: Sequence,
                 urdb_label: str,
                 solar_model: PVPlant = None,
                 wind_model: WindPlant = None,
                 storage_model: Battery = None,
                 fin_model: Singleowner = None,
                 off_grid=False,
                 fileout=None):
        """
        Initialize REopt API call

        Parameters
        ---------
        lat: float
            The latitude
        lon: float
            The longitude
        wholesale_rate_above_site_load_us_dollars_per_kwh: float
            Price of electricity sold back to the grid above the site load, regardless of net metering
        interconnection_limit_kw: float
            The limit on system capacity size that can be interconnected to grid
        load_profile: list
            The kW demand of the site at every hour (length 8760)
        urdb_label: string
            The identifier of a urdb_rate to use
        *_model: PySAM models
            Models initialized with correct parameters
        fileout: string
            Filename where REopt results should be written
        """

        self.latitude = lat
        self.longitude = lon

        self.interconnection_limit_kw = interconnection_limit_kw
        self.urdb_label = urdb_label
        self.load_profile = load_profile
        self.results = None
        self.api_key = get_developer_nrel_gov_key()

        # paths
        self.path_current = os.path.dirname(os.path.abspath(__file__))
        self.path_results = os.path.join(self.path_current)
        self.path_rates = os.path.join(self.path_current, '..', 'resource_files', 'utility_rates')
        if not os.path.exists(self.path_rates):
            os.makedirs(self.path_rates)
        # self.fileout = os.path.join(self.path_results, 'REoptResults.json')

        if fileout is not None:
            self.fileout = fileout

        if off_grid:
            self.reopt_api_url = 'https://offgrid-electrolyzer-reopt-dev-api.its.nrel.gov/v1/job/'
        else:
            self.reopt_api_url = 'https://developer.nrel.gov/api/reopt/v1/job/'

        self.post = self.create_post(solar_model, wind_model, storage_model, fin_model)
        logger.info("Created REopt post")
コード例 #4
0
ファイル: reopt.py プロジェクト: NREL/HOPP
    def get_reopt_results(self, results_file=None):
        """
        Function for posting job and polling results end-point
        :param post:
        :param results_file:
        :param API_KEY:
        :param api_url:
        :return: results dictionary / API response
        """
        if not results_file:
            results_file = self.fileout

        run_id = self.get_run_uuid(self.post, API_KEY=self.api_key, api_url=self.reopt_api_url)

        if run_id is not None:
            results_url = self.reopt_api_url + '<run_uuid>/results/?api_key=' + self.api_key
            results = self.poller(url=results_url.replace('<run_uuid>', run_id))

            with open(results_file, 'w') as fp:
                json.dump(obj=results, fp=fp)

            logger.info("Saved results to {}".format(results_file))
        else:
            results = None
            logger.error("Unable to get results: no run_uuid from POST.")

        return results
コード例 #5
0
ファイル: reopt.py プロジェクト: jlcox119/HOPP
    def create_post(self, solar_model: SolarPlant, wind_model: WindPlant, batt_model: Battery, hybrid_fin: Singleowner):
        """ The HTTP POST required by REopt"""

        post = dict()

        post['Scenario'] = dict({'user_id': 'hybrid_systems'})
        post['Scenario']['Site'] = dict({'latitude': self.latitude, 'longitude': self.longitude})
        post['Scenario']['Site']['ElectricTariff'] = self.tariff(hybrid_fin)

        if self.load_profile is None:
            self.load_profile = 8760 * [0.0]
        post['Scenario']['Site']['LoadProfile'] = dict({'loads_kw': self.load_profile})

        post['Scenario']['Site']['Financial'] = self.financial(hybrid_fin)

        post['Scenario']['Site']['PV'] = self.PV(solar_model)
        post['Scenario']['Site']['PV']['max_kw'] = self.interconnection_limit_kw

        post['Scenario']['Site']['Wind'] = self.Wind(wind_model)
        post['Scenario']['Site']['Wind']['max_kw'] = self.interconnection_limit_kw

        if batt_model is not None:
            post['Scenario']['Site']['Storage'] = self.Storage(batt_model)

        # write file to results for debugging
        post_path = os.path.join(self.path_results, 'post.json')
        if os.path.exists(post_path):
            with open(post_path, 'w') as outfile:
                json.dump(post, outfile)

            logger.info("Created REopt post, exported to " + post_path)
        return post
コード例 #6
0
 def _set_system_layout(self):
     if isinstance(self._system_model, pv_simple.Pvwattsv8):
         if self.parameters:
             self._system_model.SystemDesign.gcr = self.parameters.gcr
         if type(self.parameters) == PVGridParameters:
             self._system_model.SystemDesign.system_capacity = self.module_power * self.num_modules
             logger.info(f"Solar Layout set for {self.module_power * self.num_modules} kw")
         self._system_model.AdjustmentFactors.constant = self.flicker_loss * 100  # percent
     else:
         raise NotImplementedError("Modification of Detailed PV Layout not yet enabled")
コード例 #7
0
    def _set_system_layout(self):
        self._system_model.value("wind_farm_xCoordinates", self.turb_pos_x)
        self._system_model.value("wind_farm_yCoordinates", self.turb_pos_y)

        n_turbines = len(self.turb_pos_x)
        turb_rating = max(
            self._system_model.value("wind_turbine_powercurve_powerout"))
        self._system_model.value("system_capacity", n_turbines * turb_rating)
        logger.info(
            "Wind Layout set with {} turbines for {} kw system capacity".
            format(n_turbines, n_turbines * turb_rating))
コード例 #8
0
    def __init__(self, power_sources: dict, site: SiteInfo,
                 interconnect_kw: float):
        """
        Base class for simulating a hybrid power plant.

        Can be derived to add other sizing methods, financial analyses,
            methods for pre- or post-processing, etc

        :param power_sources: tuple of strings, float pairs
            names of power sources to include and their kw sizes
            choices include:
                    ('solar', 'wind', 'geothermal', 'battery')
        :param site: Site
            layout, location and resource data
        :param interconnect_kw: float
            power limit of interconnect for the site
        """
        self._fileout = Path.cwd() / "results"
        self.site = site
        self.interconnect_kw = interconnect_kw

        self.power_sources = dict()
        self.solar: Union[SolarPlant, None] = None
        self.wind: Union[WindPlant, None] = None
        self.grid: Union[Grid, None] = None

        if 'solar' in power_sources.keys():
            self.solar = SolarPlant(self.site, power_sources['solar'] * 1000)
            self.power_sources['solar'] = self.solar
            logger.info(
                "Created HybridSystem.solar with system size {} mW".format(
                    power_sources['solar']))
        if 'wind' in power_sources.keys():
            self.wind = WindPlant(self.site, power_sources['wind'] * 1000)
            self.power_sources['wind'] = self.wind
            logger.info(
                "Created HybridSystem.wind with system size {} mW".format(
                    power_sources['wind']))
        if 'geothermal' in power_sources.keys():
            raise NotImplementedError("Geothermal plant not yet implemented")
        if 'battery' in power_sources:
            raise NotImplementedError("Battery not yet implemented")

        self.cost_model = None

        # performs interconnection and curtailment energy limits
        self.grid = Grid(self.site, interconnect_kw)

        self.outputs_factory = HybridSimulationOutput(power_sources)
コード例 #9
0
    def calculate_installed_cost(self):
        if not self.cost_model:
            raise RuntimeError(
                "'calculate_installed_cost' called before 'setup_cost_calculator'."
            )

        solar_cost, wind_cost, total_cost = self.cost_model.calculate_total_costs(
            self.wind.system_capacity_kw / 1000,
            self.solar.system_capacity_kw / 1000)
        if self.solar:
            self.solar.set_total_installed_cost_dollars(solar_cost)
        if self.wind:
            self.wind.set_total_installed_cost_dollars(wind_cost)

        self.grid.set_total_installed_cost_dollars(total_cost)
        logger.info(
            "HybridSystem set hybrid total installed cost to to {}".format(
                total_cost))
コード例 #10
0
    def roll_timezone(self, roll_hours, timezone):
        """

        :param roll_hours:
        :param timezone:
        :return:
        """
        rollable_keys = ['dn', 'df', 'gh', 'wspd', 'tdry']
        for key in rollable_keys:
            if any(k == key for k in rollable_keys):
                roll_range = range(0, -roll_hours + 1)

                weather_array = np.array(self._data[key])

                weather_array_rolled = np.delete(weather_array, roll_range)
                weather_array_rolled = np.pad(weather_array_rolled, (0, -roll_hours + 1), 'constant')

                self._data[key] = weather_array_rolled.tolist()

        self._data['tz'] = timezone
        logger.info('Rolled solar data by {} hours for timezone {}'.format(roll_hours, timezone))
コード例 #11
0
    def __init__(self, lat, lon, year, path_resource="", filepath="", **kwargs):
        """

        :param lat: float
        :param lon: float
        :param year: int
        :param path_resource: directory where to save downloaded files
        :param filepath: file path of resource file to load
        :param kwargs:
        """
        super().__init__(lat, lon, year)

        if os.path.isdir(path_resource):
            self.path_resource = path_resource

        self.solar_attributes = 'ghi,dhi,dni,wind_speed,air_temperature,solar_zenith_angle'

        self.path_resource = os.path.join(self.path_resource, 'solar')

        # Force override any internal definitions if passed in
        self.__dict__.update(kwargs)

        # resource_files files
        if filepath == "":
            filepath = os.path.join(self.path_resource,
                                    str(lat) + "_" + str(lon) + "_psmv3_" + str(self.interval) + "_" + str(
                                        year) + ".csv")
        self.filename = filepath

        self.check_download_dir()

        if not os.path.isfile(self.filename):
            self.download_resource()

        self.format_data()

        logger.info("SolarResource: {}".format(self.filename))
コード例 #12
0
    def size_from_reopt(self):
        """
        Calls ReOpt API for optimal sizing with system parameters for each power source.
        :return:
        """
        if not self.site.urdb_label:
            raise ValueError("REopt run requires urdb_label")
        reopt = REopt(lat=self.site.lat,
                      lon=self.site.lon,
                      interconnection_limit_kw=self.interconnect_kw,
                      load_profile=[0] * 8760,
                      urdb_label=self.site.urdb_label,
                      solar_model=self.solar,
                      wind_model=self.wind,
                      fin_model=self.grid.financial_model,
                      fileout=str(self._fileout / "REoptResult.json"))
        results = reopt.get_reopt_results(force_download=False)
        wind_size_kw = results["outputs"]["Scenario"]["Site"]["Wind"][
            "size_kw"]
        self.wind.system_capacity_closest_fit(wind_size_kw)

        solar_size_kw = results["outputs"]["Scenario"]["Site"]["PV"]["size_kw"]
        self.solar.system_capacity_kw = solar_size_kw
        logger.info("HybridSystem set system capacities to REopt output")
コード例 #13
0
    def get_reopt_results(self, force_download=False):
        """
        Call the reopt_api

        Parameters
        ---------
        force_download: bool
           Whether to force a new api call if the results file already is on disk

        Returns
        ------
        results: dict
            A dictionary of REopt results, as defined
        """

        logger.info("REopt getting results")
        results = dict()
        success = os.path.isfile(self.fileout)
        if not success or force_download:
            post_url = self.reopt_api_post_url + '&api_key={api_key}'.format(
                api_key=get_developer_nrel_gov_key())
            resp = requests.post(post_url, json.dumps(self.post), verify=False)

            if resp.ok:
                run_id_dict = json.loads(resp.text)

                try:
                    run_id = run_id_dict['run_uuid']
                except KeyError:
                    msg = "Response from {} did not contain run_uuid.".format(
                        post_url)
                    raise KeyError(msg)

                poll_url = self.reopt_api_poll_url + '{run_uuid}/results/?api_key={api_key}'.format(
                    run_uuid=run_id, api_key=get_developer_nrel_gov_key())
                results = self.poller(url=poll_url)
                with open(self.fileout, 'w') as fp:
                    json.dump(obj=results, fp=fp)
                logger.info("Received REopt response, exported to {}".format(
                    self.fileout))
            else:
                text = json.loads(resp.text)
                if "messages" in text.keys():
                    logger.error("REopt response reading error: " +
                                 str(text['messages']))
                    raise Exception(text["messages"])
                resp.raise_for_status()
        elif success:
            with open(self.fileout, 'r') as fp:
                results = json.load(fp=fp)
            logger.info("Read REopt response from {}".format(self.fileout))

        return results
コード例 #14
0
ファイル: reopt.py プロジェクト: NREL/HOPP
    def poller(url, poll_interval=5):
        """
        Function for polling the REopt API results URL until status is not "Optimizing..."
        
        :param url: results url to poll
        :param poll_interval: seconds
        :return: dictionary response (once status is not "Optimizing...")
        """

        key_error_count = 0
        key_error_threshold = 3
        status = "Optimizing..."
        logger.info("Polling {} for results with interval of {}s...".format(url, poll_interval))
        while True:

            resp = requests.get(url=url, verify=False)
            resp_dict = json.loads(resp.content)

            try:
                status = resp_dict['outputs']['Scenario']['status']
            except KeyError:
                key_error_count += 1
                logger.info('KeyError count: {}'.format(key_error_count))
                if key_error_count > key_error_threshold:
                    logger.info('Breaking polling loop due to KeyError count threshold of {} exceeded.'
                             .format(key_error_threshold))
                    break

            if status != "Optimizing...":
                break
            else:
                time.sleep(poll_interval)

        if not resp.ok:
            text = json.loads(resp.text)
            if "messages" in text.keys():
                logger.error("REopt response reading error: " + str(text['messages']))
                raise Exception(text["messages"])
            resp.raise_for_status()

        return resp_dict
コード例 #15
0
 def set_total_installed_cost_dollars(self,
                                      total_installed_cost_dollars: float):
     self.financial_model.SystemCosts.total_installed_cost = total_installed_cost_dollars
     logger.info("{} set total_installed_cost to ${}".format(
         self.name, total_installed_cost_dollars))
コード例 #16
0
ファイル: site_info.py プロジェクト: NREL/HOPP
    def __init__(self, data,
                 solar_resource_file="",
                 wind_resource_file="", 
                 grid_resource_file="",
                 hub_height=97,
                 capacity_hours=[],
                 desired_schedule=[]):
        """
        Site specific information required by the hybrid simulation class and layout optimization.

        :param data: dict, containing the following keys:

            #. ``lat``: float, latitude [decimal degrees]
            #. ``lon``: float, longitude [decimal degrees]
            #. ``year``: int, year used to pull solar and/or wind resource data. If not provided, default is 2012 [-]
            #. ``elev``: float (optional), elevation (metadata purposes only) [m] 
            #. ``tz``: int (optional), timezone code (metadata purposes only) [-]
            #. ``no_solar``: bool (optional), if ``True`` solar data download for site is skipped, otherwise solar resource is downloaded from NSRDB
            #. ``no_wind``: bool (optional), if ``True`` wind data download for site is skipped, otherwise wind resource is downloaded from wind-toolkit
            #. ``site_boundaries``: dict (optional), with the following keys:

                * ``verts``: list of list [x,y], site boundary vertices [m]
                * ``verts_simple``: list of list [x,y], simple site boundary vertices [m]

            #. ``urdb_label``: string (optional), `Link Utility Rate DataBase <https://openei.org/wiki/Utility_Rate_Database>`_ label for REopt runs

            .. TODO: Can we get rid of verts_simple and simplify site_boundaries

        :param solar_resource_file: string, location (path) and filename of solar resource file (if not downloading from NSRDB)
        :param wind_resource_file: string, location (path) and filename of wind resource file (if not downloading from wind-toolkit)
        :param grid_resource_file: string, location (path) and filename of grid pricing data 
        :param hub_height: int (default = 97), turbine hub height for resource download [m]
        :param capacity_hours: list of booleans, (8760 length) ``True`` if the hour counts for capacity payments, ``False`` otherwise
        :param desired_schedule: list of floats, (8760 length) absolute desired load profile [MWe]
        """
        set_nrel_key_dot_env()
        self.data = data
        if 'site_boundaries' in data:
            self.vertices = np.array([np.array(v) for v in data['site_boundaries']['verts']])
            self.polygon: Polygon = Polygon(self.vertices)
            self.valid_region = self.polygon.buffer(1e-8)
        if 'lat' not in data or 'lon' not in data:
            raise ValueError("SiteInfo requires lat and lon")
        self.lat = data['lat']
        self.lon = data['lon']
        if 'year' not in data:
            data['year'] = 2012
        
        if 'no_solar' not in data:
            data['no_solar'] = False

        if not data['no_solar']:
            self.solar_resource = SolarResource(data['lat'], data['lon'], data['year'], filepath=solar_resource_file)

        if 'no_wind' not in data:
            data['no_wind'] = False

        if not data['no_wind']:
            # TODO: allow hub height to be used as an optimization variable
            self.wind_resource = WindResource(data['lat'], data['lon'], data['year'], wind_turbine_hub_ht=hub_height,
                                            filepath=wind_resource_file)

        self.elec_prices = ElectricityPrices(data['lat'], data['lon'], data['year'], filepath=grid_resource_file)
        self.n_timesteps = len(self.solar_resource.data['gh']) // 8760 * 8760
        self.n_periods_per_day = self.n_timesteps // 365  # TODO: Does not handle leap years well
        self.interval = int((60*24)/self.n_periods_per_day)
        self.urdb_label = data['urdb_label'] if 'urdb_label' in data.keys() else None

        if len(capacity_hours) == self.n_timesteps:
            self.capacity_hours = capacity_hours
        else:
            self.capacity_hours = [False] * self.n_timesteps

        # Desired load schedule for the system to dispatch against
        self.desired_schedule = desired_schedule
        self.follow_desired_schedule = len(desired_schedule) == self.n_timesteps
        if len(desired_schedule) > 0 and len(desired_schedule) != self.n_timesteps:
            raise ValueError('The provided desired schedule does not match length of the simulation horizon.')

            # FIXME: this a hack
        if 'no_wind' in data:
            logger.info("Set up SiteInfo with solar resource files: {}".format(self.solar_resource.filename))
        else:
            logger.info(
                "Set up SiteInfo with solar and wind resource files: {}, {}".format(self.solar_resource.filename,
                                                                                    self.wind_resource.filename))