diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aca9372..87d8d50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI_LandBOSSE +name: CI Tests # We run CI on push commits and pull requests on all branches on: [push, pull_request] @@ -9,12 +9,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Miniconda Python ${{ matrix.python-version }} - uses: conda-incubator/setup-miniconda@v3 + uses: conda-incubator/setup-miniconda@v4 with: auto-update-conda: true python-version: ${{ matrix.python-version }} @@ -23,14 +23,9 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - python -m pip install pytest pandas numpy scipy xlsxwriter openpyxl openmdao + pip install -e .[develop] - - name: Pip Install LandBOSSE - run: | - pip install -e . -v - - # Run tests - - name: Pip Run pytest + - name: Run Tests run: | pytest landbosse/tests diff --git a/.github/workflows/gh_pages.yml b/.github/workflows/gh_pages.yml index 80b6067..6a6f89a 100644 --- a/.github/workflows/gh_pages.yml +++ b/.github/workflows/gh_pages.yml @@ -28,7 +28,7 @@ jobs: jupyter-book build docs - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: "docs/_build/html" diff --git a/.github/workflows/python-publish-test.yml b/.github/workflows/python-publish-test.yml index 7067618..73ae8ae 100644 --- a/.github/workflows/python-publish-test.yml +++ b/.github/workflows/python-publish-test.yml @@ -20,12 +20,12 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.13' - name: Build package run: | diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 862358a..a01595b 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -19,12 +19,12 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.13' - name: Build package run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d12f1d..ecda8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # LandBOSSE Changelog +## 2.6.3 (May 7, 2026) + ++ Corrects an error in multiple names defined for `LandBOSSERunner`s data connection to project + Excel file, which is now called "data_tables", consistent with the implementation made in + the `WAVES` integration. ++ Removes the reliance on `component` data being provided outside the Excel project data to reduce + duplicated data definitions. ++ Corrects the `LandBOSSERunner.keys_rename` to ensure all required variables are included. ++ Makes the `weather` input to `LandBOSSERunner` optional, so that the Excel "weather_window" sheet + will still be used if provided. If `weather` is provided as an input, it will override the the + Excel-defined "weather_window" sheet. ++ `LandBOSSERunner.convert_excel_to_dict()` added for converting a project from an Excel-based + project list to a compatible dictionary. + ## 2.6.2 (February 26, 2026) + Docstrings are reformatted to display nicely on a Jupyter Book documentation site. diff --git a/README.md b/README.md index 500c149..11f834a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://badge.fury.io/py/NREL-landbosse.svg)](https://badge.fury.io/py/NREL-landbosse) [![Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![image](https://img.shields.io/pypi/pyversions/NREL-landbosse.svg)](https://pypi.python.org/pypi/NREL-landbosse) -[![Jupyter Book](https://jupyterbook.org/badge.svg)](https://nlrwindsystems.github.io/LandBOSSE) +[![Jupyter Book Badge](https://raw.githubusercontent.com/jupyter-book/jupyter-book/next/docs/media/images/badge.svg)](nlrwindsystems.github.io/LandBOSSE) ## Welcome to LandBOSSE! @@ -14,13 +14,23 @@ The methods used to develop this model (specifically, LandBOSSE Version 2.1.0) a Eberle, Annika, Owen Roberts, Alicia Key, Parangat Bhaskar, and Katherine Dykes. 2019. NREL’s Balance-of-System Cost Model for Land-Based Wind. Golden, CO: National Renewable Energy Laboratory. NREL/TP-6A20-72201. -https://www.nrel.gov/docs/fy19osti/72201.pdf. +https://www.nlr.gov/docs/fy19osti/72201.pdf. -## Installation +## Part of the WETO Stack + +LandBOSSE is primarily developed with the support of the U.S. Department of Energy and is part of the [WETO Software Stack](https://natlabrockies.github.io/WETOStack). For more information and other integrated modeling software, see: +- [Portfolio Overview](https://natlabrockies.github.io/WETOStack/portfolio_analysis/overview.html) +- [Entry Guide](https://natlabrockies.github.io/WETOStack/_static/entry_guide/index.html) +- [Techno-Economic Modeling Workshop](https://natlabrockies.github.io/WETOStack/workshops/user_workshops_2024.html#tea-and-cost-modeling) +- [Systems Engineering Workshop](https://natlabrockies.github.io/WETOStack/workshops/user_workshops_2024.html#systems-engineering) + +## User Guide + +### Installation For any installation, users should use a virtual environment. We recommend Miniconda or Anaconda, but any supporting PyPI or source installations are possible. Here, we'll work with conda for -compatibility with other NREL tools. +compatibility with other NLR tools. In the below, you can replace the name "landbosse" with any name you choose, and the Python version can be any that you prefer as long as it's supported by LandBOSSE. @@ -29,13 +39,13 @@ can be any that you prefer as long as it's supported by LandBOSSE. conda create -n landbosse python=3.13 -y ``` -### PyPI +#### PyPI ```bash pip install NREL-landbosse ``` -### Source +#### Source 1. Navigate to your preferred installation location 2. Clone the repo (or fork and clone your fork, if preferred). @@ -53,22 +63,19 @@ pip install NREL-landbosse Optional: `pip install -e .` for editable installations if you plan to modify the code itself. -## User Guides - -### First time running teh model +### First time running the model -At its most basic, the following setup is required, though the provided input data in `project_inpute_template` +At its most basic, the following setup is required, though the provided input data in `project_input_template` can be used to test out the model and view results before diving into configuring custom scenarios. -1. Create an "input" and "output" folder for LandBOSSE to access. If you are using a source +1. Create an input and output folder for LandBOSSE to access. If you are using a source installation, then ensure the folders are not located inside the local copy of the repository. 2. Create a `project_list.xlsx` like `LandBOSSE/project_list.xlsx` and a subfolder called `project_data` inside of `inputs`. 3. Each project in `project_list.xlsx` should have a corresponding Excel file in `project_data` similar to the examples in `LandBOSSE/project_input_template/project_data`. - -### Running the model +### Running the model Once the initial steps (above) are followed, we can run the model: @@ -77,18 +84,17 @@ Once the initial steps (above) are followed, we can run the model: 3. Run the model: `python main.py -i input-folder-path -o output-folder-path` (be sure to replace "input-folder-path" and "output-folder-path" with your respective input and output folders). -All together + All together, this is: -```bash -conda activate landbosse -cd /path/to/LandBOSSE -python main.py -i /path/to/inputs -o /path/to/outputs -conda deactivate -``` + ```bash + conda activate landbosse + cd /path/to/LandBOSSE + python main.py -i /path/to/inputs -o /path/to/outputs + conda deactivate + ``` 4. View your results in the output folder. - ### Integrating LandBOSSE into your code While LandBOSSE was originally designed as a CLI tool powered by Excel workbooks, an API also exists diff --git a/docs/_config.yml b/docs/_config.yml index fe2d496..a80cb01 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -30,6 +30,16 @@ repository: html: use_issues_button: true use_repository_button: true + home_page_in_navbar: true + +parse: + myst_url_schemes: [mailto, http, https] + myst_enable_extensions: + - dollarmath + - amsmath + - deflist + - linkify + - colon_fence sphinx: extra_extensions: @@ -46,25 +56,6 @@ sphinx: show_toc_level: 2 repository_url: "https://github.com/NLRWindSystems/LandBOSSE" repository_branch: main - # icon_links: [ - # { - # name: GitHub, - # url: "https://github.com/NREL/WAVES", - # icon: fa-brands fa-github, - # }, - # { - # name: PyPI version, - # url: "https://pypi.org/project/WAVES/", - # icon: "https://img.shields.io/pypi/v/WAVES?link=https%3A%2F%2Fpypi.org%2Fproject%2FWAVES%2F", - # type: url, - # }, - # { - # name: Binder, - # url: "https://mybinder.org/v2/gh/NREL/WAVES/main?filepath=examples", - # icon: "https://mybinder.org/badge_logo.svg", - # type: url, - # }, - # ] language: 'python' autosummary_generate: true autodoc_default_options: diff --git a/docs/_toc.yml b/docs/_toc.yml index 2004a87..95a9f2e 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -4,6 +4,6 @@ format: jb-book root: intro chapters: -- file: api -- file: examples -- file: contributing \ No newline at end of file +- file: example +- file: contributing +- file: api \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md index 3302eb1..a051e5d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,3 +1,42 @@ -# Contributor's Guide +# Developer's Guide -Coming soon. \ No newline at end of file +## Installation + +1. Navigate to your preferred installation location +2. Fork LandBOSSE +3. Clone your fork + + ```bash + git clone https://github.com//LandBOSSE.git + ``` + +4. Enter the directory and locally install an editable version with the testing and documentation + building dependencies. + + ```bash + cd LandBOSSE + pip install -e .[test, docs] + ``` + +## Testing the Code + +Run LandBOSSE's testing suite using the following command. Please note the tests are not +particularly robust at this time, so passing tests after modifying the code should be viewed with +caution. + +```bash +pytest landbosse +``` + +## Building the documentation + +LandBOSSE uses Jupyter Book (v1) for its documentation site, which can be recreated locally with +the following command from the top-level directory. + +```bash +jupyter book build docs/ +``` + +Once complete, there will be a callout to "paste this line directly into your browser bar" with the +next line in the form of `file:///path/to/LandBOSSE/docs/_build/html/index.html`, which should be +pasted into the browser bar like a URL. You should now be able to peruse the documentation. diff --git a/docs/example.md b/docs/example.md new file mode 100644 index 0000000..eb85fe2 --- /dev/null +++ b/docs/example.md @@ -0,0 +1,727 @@ +# User Guide + +This example will work with the `project_input_template/project_list_simplified.xlsx`, which +includes the GE 1.5 MW validation example (`project_input_template/project_data/ge15_public.xlsx`). +Using these data, we will walk through the setup of the project-specific data, project listings, +and running the model. + +## Project Input Data + +LandBOSSE relies on the use of separate input and output folders. For any set of projects, these +can be defined as environment variables using the names `LANDBOSSE_INPUT_DIR` and +`LANDBOSSE_OUTPUT_DIR`, or provided when running the code (more details in [Running LandBOSSE](#running-landbosse)). + +An example of a project input folder is provided with the code: the folder `project_input_template/` +where listings of projects can be stored and the subfolder `project_data` where all project-specific +Excel files are stored. + +### Project listing and the `LandBOSSERunner` API + +As stated in the introduction, we'll be working with `project_list_simplified.xlsx` which contains +a single project (unlike `project_list.xlsx` which runs several projects simultaneously), the +public-facing GE 1.5 MW example (`ge15_public.xlsx`). + +The below table explains each of the expected parameters found when setting up either the +`project_list.xlsx` or the `LandBOSSERunner` input dictionary. For Excel setups, each row in +"Project List Column" represents a column name in the project list Excel file. Using the Excel setup, +we can run multiple projects, though this example relies models a single project. Similarly, using +the `LandBOSSERunner` configuration dictionary, each row in "LandBOSSE Runner Key" represents a +dictionary key. Notably, using the `LandBOSSERunner` API enables single project runs only. In both +cases the "Description" column describes what data are expected as an Excel sheet's cell value or +dictionary key's value. + +| Project List Column | LandBOSSE Runner Key | Description | +| -------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Project ID | id | Name of the project | +| Project data file | data_tables | Excel filename associated with the project | +| Total project construction time (months) | construction_months | Total construction duration (months) | +| Turbine rating MW | turbine_rating_MW | Capacity of turbine (MW) | +| Hub height m | hub_height_m | Turbine hub height (m) | +| Rotor diameter m | rotor_diameter_m | Turbine rotor diameter (m) | +| Turbine spacing (times rotor diameter) | turbine_spacing_rotor_diameter | Layout spacing between turbines in each row, in rotor diameters | +| Row spacing (times rotor diameter) | row_spacing_rotor_diameter | Layout spacing between rows, in rotor diameters | +| Number of turbines | num_turbines | Number of turbines | +| Turbine Capex (USD/kW) | turbine_capex | CapEx of turbine ($/kW) | +| Breakpoint between base and topping (percent) | base_topping_breakpoint | Height at which cranes change from 'Base' to 'Topping', represented as a fraction of hub height | +| Fuel cost USD per gal | fuel_cost_usd_per_gal | Cost of fuel per gallon ($/gal) | +| Rate of deliveries (turbines per week) | turbine_delivery_rate_per_week | Number of turbine deliveries (all components) per week | +| Wind shear exponent | wind_shear | Wind shear (assumed constant across all timestamps and turbines) | +| Foundation depth m | foundation_depth_m | Depth of concrete foundation (m) | +| Rated Thrust (N) | rated_thrust_N | Rated horizontal thrust of turbine (N) | +| Bearing Pressure (n/m2) | bearing_pressure_Pa | Bearing pressure capacity of soil underneath foundations (Pa) | +| 50-year Gust Velocity (m/s) | gust_velocity_50_year_m/s | 50 year wind gust velocity (m/s) | +| Line Frequency (Hz) | line_frequency | Electrical frequency of grid (Hz) | +| Combined homerun trench length to substation (km) | combined_homerun_trench_length_km | Combined homerun trench length to substation (km) (optional - can be null but cannot be omitted) | +| Flag for user-defined home run trench length (0 = no; 1 = yes) | user_trench_length_flag | Flag (0 = no; 1 = yes) to indicate if the user-defined trench length should be used | +| Non-Erection Wind Delay Critical Height (m) | non_erection_wind_delay_critical_height_m | When calculating wind delays for non-erection tasks, shear values to this height (m) | +| Non-Erection Wind Delay Critical Speed (m/s) | non_erection_wind_delay_critical_wind_speed_m/s | When calculating wind delays for non-erection tasks, wind speeds above this value will force work to stop (m/s) | +| Distance to interconnect (miles) | distance_to_interconnect_mi | Distance from substation to switchyard (miles) | +| Interconnect Voltage (kV) | interconnect_voltage_kV | Voltage of grid (kV) | +| New Switchyard (y/n) | new_switchyard | Should a new switchyard be built (true/false) | +| Road length adder (m) | road_length_adder_m | Distance from site entry point to the project (m) | +| Road Quality (0-1) | road_quality | Non-dimensional representation of the quality of roads. 0 is poor quality, 1 is good quality. A higher road quality reduces the total road cost | +| Percent of roads that will be constructed | percent_roads_to_be_constructed | What percentage of roads need to be constructed vs already exist | +| Road width (ft) | road_width_ft | Width of roads (ft) | +| Road thickness (in) | road_thickness_in | Thickness of roads (in) | +| Calculate road cost for distributed wind? (y/n) | calculate_road_cost_for_distributed_wind | Should road costs be included in the calculation for distributed wind projects (true/false) | +| Site prep area for Distributed wind (m2) | site_prep_area_for_distributed_wind_m2 | Site prep area for distributed wind projects (m^2) | +| Crane width (m) | crane_width_m | Width of crane (m) | +| Number of highway permits | num_highway_permits | Number of highway permits required | +| Number of access roads | num_access_roads | Number of site access roads required | +| Overtime multiplier | overtime_multiplier | Labor cost multiplier for overtime work | +| Allow same flag | allow_same_flag | flag to indicate whether choosing same base and topping crane is allowed (true/false) | +| Override total management cost for distributed (0 does not override) | override_total_management_cost_for_distributed | For distributed wind project, override the calculated management cost and use this value instead (0 means do not override) | +| Markup contingency | markup_contingency | Markup contingency | +| Markup warranty management | markup_warranty_management | Markup warranty management | +| Markup sales and use tax | markup_sales_and_use_tax | Markup sales and use tax | +| Markup overhead | markup_overhead | Markup overhead | +| Markup profit margin | markup_profit_margin | Markup profit margin | +| Utility Interconnection Fees (Small DW only) | utility_interconnection_fees_distributed_wind | Fee for connecting to the grid (distributed wind projects only) | +| Labor cost multiplier | labor_cost_multiplier | Multiplier to modify labor costs | +| Crane breakdown fraction | crane_breakdown_fraction | What fraction of cranes will breakdown. 0 means none, 1 means all. Breakdowns increase the total erection duration | +| No mapping available | enable_cost_and_scaling_modifications | Should cost and scaling modifications be applied (true/false) | + +### Project Excel Data + +Regardless of how the above data are provided, an Excel file will be required for individual project +configurations. These data will contain various cost, logistics, site, and engineering data. Each +of the following subsections will show up to the first 10 rows of the data in `ge15_public.xlsx` +to demonstrate the data and formats required for the input data. + +For each sheet below, except `weather_window`, additional columns can be provided after the listed, +required columns. These can be things such as notes and sources that are helpful for tracking +source data and any methodologies for transforming them to fit the LandBOSSE formats. In many +of the sheets in `ge15_public.xlsx`, there will be one or multiple notes and sources columns for +exactly this purpose. However, those data are not exemplified below. + +#### `components` + +The `components` sheet contains detailed information about any large scale, assembled on-site +component, such as tower sections, the nacelle, or blades. Each individual component should be +recorded in a single row (i.e. each blade gets 1 row even if they are exactly the same). + +Component +: Name of the component; can be specific or generic. For items where there are more than one of them + or they are are part of a whole like tower sections or blades, number them using 1-indexing, + e.g., "Tower 1", "Tower 2", "Tower 3". + +Mass tonne +: Mass in metric tonnes. + +Lift height m +: Height, in $m$, that a crane will have to lift the component in order to complete assembly. + +Surface area sq m +: Total surface area, in $m^2$, of a cross-section of the component. This will be used for + calculating the foundational load. + +Coeff drag +: Coefficient of drag. This will be used to understand the effective mass while lifting the component. + +Coeff drag (installed) +: Coefficient of drag once the component is installed. This will be used for calculating the + foundational load. + +Section height m +: Total height ($m$) of a cross-section of the component. This will be used for calculating the + foundational load. + +Lever arm m +: Perpendicular distance, in $m$, from center of the foundation. + +Cycle time installation hrs +: Amount of time, in hours, required to prepare the component for assembly. + +Offload hook height m +: Height of the hook ($m$) above the top of the component to account for the total height of the crane's lift. + +Offload cycle time hrs +: Amount of time (in hours) required to detach the component from the crane. + +Multiplier drag rotor +: Multiplier for accounting for the additional drag of the rotor for computing the foundation load. + This should only apply to the nacelle and blades + +Multiplier tower drag +: Multiplier for accounting for the additional drag of the tower for computing the foundation load. + +The following data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Component | Mass tonne | Lift height m | Surface area sq m | Coeff drag | Coeff drag (installed) | Section height m | Lever arm m | Cycle time installation hrs | Offload hook height m | Offload cycle time hrs | Multiplier drag rotor | Multiplier tower drag | +|-------------------|--------------|-----------------|---------------------|--------------|--------------------------|--------------------|---------------|-------------------------------|-------------------------|--------------------------|------------------------|-------------------------| +| Nacelle GE 1.5SLE | 50 | 90 | 33 | 0.8 | 0.8 | 0 | 80 | 1.5 | 6 | 0.5 | 1 | 0 | +| Hub | 15.4 | 90 | 11.3 | 1.1 | 1.1 | 0 | 80 | 1 | 6 | 0.5 | 0 | 0 | +| Blade 1 | 5.2 | 90 | 33.44 | 0.1 | 1.4 | 0 | 80 | 1 | 6 | 0.5 | 0.666667 | 0 | +| Blade 2 | 5.2 | 90 | 33.44 | 0.1 | 1.4 | 0 | 80 | 1 | 6 | 0.5 | 0.666667 | 0 | +| Blade 3 | 5.2 | 90 | 33.44 | 0.1 | 1.4 | 0 | 80 | 1 | 6 | 0.5 | 0.666667 | 0 | +| Tower section 1 | 59.8 | 30 | 95.89 | 0.6 | 1.1 | 25 | 12 | 1 | 6 | 0.5 | 0 | 1 | +| Tower section 2 | 39.3 | 60 | 96.25 | 0.6 | 1.1 | 25 | 37 | 1 | 6 | 0.5 | 0 | 1 | +| Tower section 3 | 30.9 | 90 | 90 | 0.6 | 1.1 | 30 | 65 | 1 | 6 | 0.5 | 0 | 1 | + +#### `cable_specs` + +The `cable_specs` sheet should contain information about the array cables potentially used on site. +The specific types and quantities will be sized according to LandBOSSE's collection system. + +Array Cable +: Name or type of the array cable. + +Conductor Size (mm2) +: Cable cross section, in $mm^2$. + +Current Capacity (A) +: Cable current capacity at 1m burial depth, in $A$. + +Rated Voltage (V) +: Cable rated line-to-line voltage, in $V$. + +AC Resistance (Ohms/km) +: Cable resistance for the AC current per kilometer, in $\frac{\omega}{km}$ + +Inductance (mH/km) +: Cable inductance per kilometer, in $\frac{mH}{km}$ + +Capacitance (nF/km) +: Cable capacitance per kilometer, in $\frac{nF}{km}$ + +Cost (USD/LF) +: Cost of the cable per linear foot ($\frac{USD}{LF}$). + +The following data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Array Cable | Conductor Size (mm2) | Current Capacity (A) | Rated Voltage (V) | AC Resistance (Ohms/km) | Inductance (mH/km) | Capacitance (nF/km) | Cost (USD/LF) | +|---------------|------------------------|------------------------|---------------------|---------------------------|----------------------|-----------------------|-----------------| +| AWG 1/0 | 120 | 300 | 36 | 0.253 | 0.398 | 0.179 | 6 | +| AWG 4/0 | 240 | 440 | 36 | 0.125 | 0.359 | 0.223 | 9 | +| MCM 500 | 500 | 640 | 36 | 0.0605 | 0.317 | 0.293 | 13 | +| MCM1000 | 800 | 830 | 36 | 0.0367 | 0.291 | 0.375 | 16 | +| MCM1250 | 1000 | 935 | 36 | 0.0291 | 0.284 | 0.411 | 17 | + +#### `equip` + +The `equip` sheet is for information about the equipment grouping required for each general operational +category. The optimal equipment will be chosen based on a comparison of the equipment grouping's +capabilities and the components being lifted. + +Equipment ID +: Unique identifier for the equipment group. + +Operation +: One of "Top", "Base", or "Offload" to indicate what grouping of operations the equipment can + perform. + +Equipment name +: Name of the equipment in the equipment group. + +Crane capacity tonne +: Maximum capacity of the crane, in metric tonnes. + +Number of equipment +: Number of the equipment used during the operation. + +The following subset of the data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Equipment ID | Operation | Equipment name | Crane capacity tonne | Number of equipment | +|----------------|-------------|------------------|------------------------|-----------------------| +| E1 | Base | Crawler crane | 500 | 1 | +| E1 | Base | Truck crane | 50 | 1 | +| E1 | Top | Crawler crane | 500 | 1 | +| E1 | Top | Truck crane | 50 | 2 | +| E2 | Top | Crawler crane | 600 | 1 | +| E2 | Top | Truck crane | 50 | 2 | +| E2 | Top | RT | 100 | 2 | +| E3 | Top | Crawler crane | 750 | 1 | +| E3 | Top | RT | 100 | 2 | +| E4 | Top | Crawler crane | 1000 | 1 | +| ... | ... | ... | ... | ... | + +#### `crane_specs` + +The `crane_specs` sheet should contain all the detailed information about a crane's operating +limits and capacities. + +Equipment name +: Name matching with the `equip` sheet. + +Crane name +: Crane model name + +Boom system +: Type of boom system used on the crane. Primarily used for logging crane details. + +Crane capacity tonne +: Maximum crane capacity. Used for matching cranes to components for ideal assembly costs and speeds. + +Speed of travel km per hr +: Ground travel speed, in $km/h$ + +Hoist speed m per min +: Crane lifting rate, in $m/min$. + +Crew type ID +: Unique identifier for the type of crew required for the operation. + +Equipment ID +: Unique identifier matching with the `equip` sheet. + +Setup time hr +: Amount of time, in hours, required for the crane to be prepared at a new lift location. + +Breakdown time hr +: Amount of time, in hours, required for the crane to be broken down prior to being moved to a new + lift location. + +Max wind speed m per s +: Maximum allowable wind speed allowed during operations, in $m/s$. + +Hub height m +: Maximum lift height for the crane, in $m$. + +Max capacity tonne +: Maximum lift capacity for the crane, in metric tonnes. + +Radius m +: Undefined, and potentially unused. + +Hook Height m +: Height of the hook above the top of the component to be lifted. Used for the total lift mass and + wind speed calculations. + +Mobilization cost USD +: Cost to mobilize the crane for the duration of the installation period, in USD. + +The following subset of the data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Equipment name | Crane name | Boom system | Crane capacity tonne | Speed of travel km per hr | Hoist speed m per min | Crew type ID | Equipment ID | Setup time hr | Breakdown time hr | Max wind speed m per s | Hub height m | Max capacity tonne | Radius m | Hook Height m | Mobilization cost USD | +|------------------|--------------|------------------|------------------------|-----------------------------|-------------------------|----------------|----------------|-----------------|---------------------|--------------------------|----------------|----------------------|------------|-----------------|-------------------------| +| Offload crane | LB 75 | Hydraulic | 75 | 40 | 60 | C0 | OL1 | 0 | 0 | 10 | 12.5 | 38 | 5.4 | 12.5 | 8040 | +| Offload crane | Big LB 2580 | Big Hydraulic | 500 | 40 | 60 | C0 | OL2 | 0 | 0 | 10 | 48 | 500 | 11 | 48 | 406100 | +| Offload crane | LB 258 | Hydraulic | 200 | 40 | 60 | C0 | OL2 | 0 | 0 | 10 | 48 | 63 | 11 | 48 | 40610 | +| Crawler crane | M999 | 22EL | 275 | 2 | 20 | C1 | B1 | 0 | 40 | 9 | 57 | 67 | 14 | 57 | 68709 | +| Crawler crane | LR1500 | SL3F | 500 | 2 | 20 | C1 | E1 | 0 | 40 | 9 | 80 | 102 | 16 | 94 | 184398 | +| Crawler crane | LR1500 | SL3F | 500 | 2 | 20 | C1 | E1 | 0 | 40 | 9 | 85 | 95 | 16 | 100 | 184398 | +| Crawler crane | LR1500 | SL3F | 500 | 2 | 20 | C1 | E1 | 0 | 40 | 9 | 90 | 87 | 16 | 106 | 184398 | +| Crawler crane | LR1500 | SL3F | 500 | 2 | 20 | C1 | E1 | 0 | 40 | 9 | 100 | 77 | 18 | 112 | 184398 | +| Crawler crane | LR1500 | SL4DFB + Derrick | 500 | 2 | 20 | C1 | E1 | 2 | 40 | 9 | 80 | 112 | 16 | 94 | 184398 | +| Crawler crane | LR1500 | SL4DFB + Derrick | 500 | 2 | 20 | C1 | E1 | 2 | 40 | 9 | 90 | 105 | 16 | 100 | 184398 | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | + +#### `development` + +The `development` sheet contains any project development costs that are unaccounted for in the other +project specifications. + +Type of cost +: Development cost category. + +Cost USD +: Total cost, in USD. + +Phase of construction +: Phase of construction where the cost is incurred, likely alway "Development". + +The following data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Type of cost | Cost USD | Phase of construction | +|------------------|------------|-------------------------| +| Equipment rental | 0 | Development | +| Labor | 1000000 | Development | +| Materials | 0 | Development | +| Mobilization | 0 | Development | +| Other | 0 | Development | + +#### `crew_price` + +The `crew_price` sheet contains information about the hourly and daily costs of having a single crew +member on site for each labor category. + +Labor type ID +: Unique labor type identifier, matching with `crew`. + +Hourly rate USD per hour +: Hourly cost of the crew per hour, in $USD/hr$. + +Per diem USD per day +: Daily cost for food, etc. for the type of crew member, in $USD/day$. + +The following subset of the data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Labor type ID | Hourly rate USD per hour | Per diem USD per day | +|----------------------|----------------------------|------------------------| +| Crane operator | 81.9105 | 149 | +| Oiler | 61.625 | 149 | +| Rigger | 85.84 | 149 | +| Truck driver | 66.41 | 149 | +| Iron worker | 94.5545 | 149 | +| Project manager | 119 | 149 | +| Site manager | 112.2 | 149 | +| Construction manager | 112.2 | 149 | +| Project engineer | 93.5 | 149 | +| Safety or qc manager | 106.118 | 149 | +| ... | ... | ... | + +#### `crew` + +The `crew` sheet is for information about the amount and type of labor required in a crew category. + +Crew type ID +: Unique crew type identifier, matching with `crane_specs` + +Operation +: General operational category for the type of crew member. + +Crew name +: Name of the crew type. + +Labor type ID +: Unique labor type identifier for the type of crew member. + +Number of workers +: Number of workers in the labor type that make up one unit of the crew. + +The following subset of the data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Crew type ID | Operation | Crew name | Labor type ID | Number of workers | +|----------------|-----------------------|--------------------------------|----------------------|---------------------| +| M0 | Management | Management - project size | Project manager | 1 | +| M0 | Management | Management - project size | Site manager | 1 | +| M0 | Management | Management - project size | Construction manager | 1 | +| M0 | Management | Management - project size | Project engineer | 1 | +| M0 | Management | Management - project size | Safety or qc manager | 1 | +| M0 | Management | Management - project size | Logistics manager | 1 | +| M0 | Management | Management - project size | Office admin | 1 | +| M1 | Management | Management - rate construction | Tool room | 1 | +| M1 | Management | Management - rate construction | Electrician | 2 | +| MC0 | Mechanical completion | Mechanical completion | QC/QA tech | 2 | +| ... | ... | ... | ... | ... | + +#### `equip_price` + +The `equip_price` sheet is for information about the operational costs for a single crane. + +Equipment name +: Name of the equipment. + +Crane capacity tonne +: Maximum capacity of the crane, in metric tonnes. + +Equipment price USD per hour +: Hourly cost of the equipment, in $USD/hr$. + +Cost USD per breakdown +: Total cost to breakdown the equipment before moving to another lift site, in USD. + +Fuel consumption gal per day +: Expected daily fuel consumption, in $gal/day$. + +The following subset of the data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Equipment name | Crane capacity tonne | Equipment price USD per hour | Cost USD per breakdown | Fuel consumption gal per day | +|------------------|------------------------|--------------------------------|--------------------------|--------------------------------| +| Mobile crane | 75 | 35 | nan | 44 | +| Mobile crane | 200 | 149 | nan | 64 | +| Crawler crane | 275 | 217 | 37674 | 76 | +| Crawler crane | 250 | 194 | 30534 | 72 | +| Crawler crane | 400 | 330 | 73375 | 97 | +| Crawler crane | 500 | 420 | 101936 | 113 | +| Crawler crane | 600 | 511 | 130497 | 130 | +| Crawler crane | 750 | 647 | 173339 | 154 | +| Crawler crane | 1000 | 873 | 244741 | 195 | +| Crawler crane | 1350 | 1190 | 344705 | 252 | +| ... | ... | ... | ... | ... | + +#### `material_price` + +The `material_price` sheet is about the construction materials costs for aspects such as road and +foundation building, not turbine components. + +Material type ID +: Unique material identifier + +Material price USD per unit +: Cost of the materials per unit, in $USD/unit$. + +Unit +: Measurement units used for measuring the material's quantity. + +The following data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Material type ID | Material price USD per unit | Unit | +|------------------------------------|-------------------------------|-------------------------------| +| unique identifier for the material | price of material per unit | unit of measure used for cost | +| Concrete 3000 psi | 117 | cubic yard | +| Concrete 5000 psi | 140 | cubic yard | +| Concrete 8000 psi | 130 | cubic yard | +| Steel - rebar | 1120 | ton (short) | +| Road base - 3/4 inch crushed stone | 15 | Loose cubic yard | +| Excavated dirt | 0 | cubic yard | +| Backfill | 0 | cubic yard | + +#### `rsmeans` + +The `rsmeans` sheet for any relevant RSMeans operational cost data. + +Operation ID +: Unique identifier for the operation. + +Type of cost +: Cost category. + +Material type ID +: Type of material used, should match `material_price`. + +Rate USD per unit +: Cost per unit, in $USD/unit$ + +Units +: Measurement units. + +Daily output +: Daily output of the operation. + +Per Diem Hours (per unit) +: Additional hourly per diem costs per measured unit. + +Module +: Construction phase category. Should be one of "Foundations", "Roads", or "Collection". + +Number of workers +: Number of workers required for the operation type. + +The following subset of data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Operation ID | Type of cost | Material type ID | Rate USD per unit | Units | Daily output | Per Diem Hours (per unit) | Module | Number of workers | +|----------------------------------------------|------------------|------------------------------------|---------------------|------------------------------|----------------|-----------------------------|-------------|---------------------| +| Concrete placement | Equipment rental | Concrete 5000 psi | 4.61 | $/cubic yard | | | Foundations | | +| Rebar installation | Equipment rental | Steel - rebar | 0 | $/ton (short) | | | Foundations | | +| Survey | Equipment rental | | | | | | Roads | | +| Clear and grub | Equipment rental | | | | | | Roads | | +| Topsoil stripping and stockpiling | Equipment rental | | 1.63 | cubic yard | | 0 | Roads | 1 | +| Stormwater pollution prevention | Equipment rental | | | | | | Roads | | +| Culverts | Equipment rental | | | | | | Roads | | +| Compaction of soil (subgrade and crane path) | Equipment rental | | 1.15 | embankment cubic yards crane | | 0 | Roads | 3 | +| Mass material movement (cut and fill) | Equipment rental | | | | | | Roads | | +| Placing road base (hauling) | Equipment rental | Road base - 3/4 inch crushed stone | 5.16 | loose cubic yard | | 0 | Roads | 1 | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | + +#### `site_facility_building_area` + +The `site_facility_building_area` is for sizing the wind farm's on-site facility appropriately for +the wind power plant's size. + +Size Min (MW) +: Minimum wind power plant size to estimate the site facility building size, in $MW$. + +Size Max (MW) +: Maximum wind power plant size to estimate the site facility building size, in $MW$. + +Building area (sq. ft.) +: Building size for site facilities, in ${ft}^2$. + +The following data are used for the GE 1.5 MW public example (`ge15_public.xlsx`). + +| Size Min (MW) | Size Max (MW) | Building area (sq. ft.) | +|-----------------|-----------------|---------------------------| +| 0 | 200 | 3000 | +| 200 | 500 | 5000 | +| 500 | 800 | 7000 | +| 800 | 1000 | 9000 | +| 1000 | 5000 | 12000 | + +#### `weather_window` + +The hourly weather profiles uses 4 columns with 3 header rows formatted like the following table. +In the following data, taken from the GE 1.5 MW public example (`ge15_public.xlsx`), the first +row should contain the measurement units, and the second row should indicate the height at which +the measurement was taken, in $m$. + +| | Temperature | Pressure | Direction | Speed | +| ------------------- | ----------- | ---------- | --------- | ------ | +| | C | atm | Degrees | m/s | +| | 100 | 100 | 100 | 100 | +| 2012-01-01 12:00:00 | 2.444 | 0.95205318 | 185 | 8.681 | +| 2012-01-01 13:00:00 | 1.655 | 0.95227456 | 270 | 7.234 | +| 2012-01-01 14:00:00 | 2.512 | 0.9524854 | 322 | 12.898 | +| 2012-01-01 15:00:00 | 2.589 | 0.953325 | 321 | 14.795 | +| 2012-01-01 16:00:00 | 1.534 | 0.95440628 | 323 | 15.076 | +| 2012-01-01 17:00:00 | 1.014 | 0.95551016 | 322 | 15.53 | +| ... | ... | ... | ... | ... | + +## Running LandBOSSE + +The two primary means for running LandBOSSE are through the terminal using only Excel-based data, +or through Python (script, IDE, Jupyter Notebook, etc.). The following sections will walk through +running LandBOSSE through each of these methods. + +### Terminal + +Per the installation instructions, this example assumes your conda (or other) Python environment +has been created and LandBOSSE has been installed. + +1. Determine the desired input and output folder locations for your data, ensuring the input folder + contains project listing Excel sheets in the top-level of this folder, and all project-specific + Excel data in the `project_data` subfolder. This should mirror the + [repository's `project_input_template`](https://github.com/NLRWindSystems/LandBOSSE/tree/main/project_input_template). +2. Configure your project listing Excel file and the project data Excel file for each listed project + described in [Project Input Data](#project-input-data). + + :::{important} The name of the project listing must be called `project_list.xlsx` as it will + be the only file that is used for running projects. + ::: + +3. Open a terminal (or Anaconda Prompt or other) session. +4. Navigate to where LandBOSSE has been downloaded. In the terminal: + + ```bash + cd /path/to/LandBOSSE + ``` + +5. Activate your Python environment. For conda environments, but be sure to use the name of your + environment: + + ```bash + conda activate landbosse + ``` + +6. Run all validLandBOSSE, where `-i` can be substituted for `--input`, and `-o` for `--output`. + + ```bash + python main.py -i /path/to/input-folder -o /path/to/output-folder + ``` + + Additional run flags also exist: + + - `-s` or `--scaling` for scaling study mode. This method modifies each row of the project list + after it has been modified by the parameters for to scale certain input values based on what + has been parametrically modified. + - `-v` or `--validate` which creates the file path for the output file from prior LandBOSSE run + that will be used to check latest run. The validation output file must be in the inputs folder + and must be called `landbosse-output-validation.xlsx`. + +### `LandBOSSERunner` + +#### Converting The Excel Project List To A Dictionary + +The below code snippet demonstrates how to convert the project list file for a given project, such +as the "foundation_validation_ge15" project found in `project_list_simplified.xlsx` that is +exemplified throughout this walkthrough. Please read the in-code comments for further context +about what steps are being taken and why. + +```python +import pandas as pd + +from landbosse.landbosse_runner import LandBOSSERunner + +inputs = LandBOSSERunner( + filename="/path/to/LandBOSSE/project_input_template/project_list_simplified.xlsx", + project_id="foundation_validation_ge15", + enable_cost_and_scaling_modifications=False, +) + +# Add a missing value from the Excel data that is used in the dictionary-based model +inputs["turbine_capex"] = 3500 + +# Load the data tables +# NOTE: the Excel input data as provided in this repository contains neither the path to +# the file, nor the extension, so they must be added here +inputs["data_tables"] = pd.read_excel( + f"/path/to/LandBOSSE/project_input_template/project_data/{inputs['data_tables']}.xlsx", + sheet_name=None, +) +``` + +#### Running In A Python Script + +1. Determine the desired input and output folder locations for your data, ensuring the input folder + contains project listing Excel sheets in the top-level of this folder, and all project-specific + Excel data in the `project_data` subfolder. This should mirror the + [repository's `project_input_template`](https://github.com/NLRWindSystems/LandBOSSE/tree/main/project_input_template). +2. Configure your project listing Excel file and the project data Excel file for each listed project + described in [Project Input Data](#project-input-data). +3. Manually load the project data and run the project (single workflow example after step-by-step + instructions). + + 1. Import the required dependencies. + + ```python + from pathlib import Path + + import yaml + import pandas as pd + + from landbosse.landbosse_runner import LandBOSSERunner + ``` + + 2. Optional: Load the hourly weather profile. + + ```python + weather = pd.read_csv("/my/weather/data.csv") + weather = LandBOSSERunner.add_header_to_weather_dataframe(weather) + ``` + + 3. Load the project listing Excel data. + + ```python + with Path("/path/to/my_project_data.yaml").open() as f: + inputs = yaml.safe_load(f) + ``` + + 4. Load the single project's Excel data and connect it to the project listing dictionary above. + + ```python + data_path = Path("/path/to/data_tables/").resolve() + inputs["data_tables"] = pd.read_excel( + data_path /inputs["data_tables"], sheet_name=None + ) + ``` + + 5. Create the LandBOSSE object and run + + ```python + lb = LandBOSSERunner(input_config=inputs, weather=weather) + lb.run() + ``` + + As a single, combined workflow, the below can serve as a base workflow for most projects. + + ```python + from pathlib import Path + + import yaml + import pandas as pd + + from landbosse.landbosse_runner import LandBOSSERunner + + # If your data are stored in a YAML file (preferable), load them first, or + # create a data dictionary in place of the following code. + with Path("/path/to/my_project_data.yaml").open() as f: + inputs = yaml.safe_load(f) + + # Load the Excel project data. If the value of "data_tables" is relative to where the + # code will be run, simply exclude the `data_path` portion of the following code. + data_path = Path("/path/to/data_tables/").resolve() + inputs["data_tables"] = pd.read_excel( + data_path /inputs["data_tables"], sheet_name=None + ) + + # Optional: load alternative weather data + weather = pd.read_csv("/my/weather/data.csv") # replace csv with your file format + weather = LandBOSSERunner.add_header_to_weather_dataframe(weather) + + # Create your runner object, and run the analysis. Simply exclude the weather + # keyword argument if yours is contained in the `data_tables` + lb = LandBOSSERunner(input_config=inputs, weather=weather) + lb.run() + ``` + +4. Once the above code (or similar) is successfully run, the `result` object is created and attached + to the `LandBOSSERunner`. For complete details please see the `LandBOSSEResult` API documentation. + The `lb.result` (using the above examples naming convention for the runner object) contains the + following attributes as separate Pandas Series or DataFrames, and can be saved to CSV or one + Excel book as desired. + + - `project_parameters`: The values provided from the input dictionary. + - `data_sheets`: The Excel project data loaded as a dictionary of dataframes. + - `model_variables`: A copy of the model details that would traditionally be saved to Excel + when running LandBOSSE through the terminal. + - `operation_cost`: The detailed operational cost logs for the simulated installation. diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index b557eef..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,3 +0,0 @@ -# Examples - -Coming soon \ No newline at end of file diff --git a/docs/intro.md b/docs/intro.md index 2b26499..e8aa3d2 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1,2 +1,2 @@ :::{include} ../README.md -::: \ No newline at end of file +::: diff --git a/landbosse/__init__.py b/landbosse/__init__.py index 45acc53..dfb23b3 100644 --- a/landbosse/__init__.py +++ b/landbosse/__init__.py @@ -1,2 +1,2 @@ -__version__ = "2.6.2" +__version__ = "2.6.3" diff --git a/landbosse/excelio/XlsxReader.py b/landbosse/excelio/XlsxReader.py index a8cf4b0..5502239 100644 --- a/landbosse/excelio/XlsxReader.py +++ b/landbosse/excelio/XlsxReader.py @@ -18,27 +18,16 @@ class XlsxReader: of data is read from the following sheets: - components - - cable_specs - - equip - - crane_specs - - development - - crew_price - - crew - - equip_price - - material_price - - rsmeans - - site_facility_building_area - - weather_window The second set of data are read from a single sheet as described below. diff --git a/landbosse/landbosse_omdao/OpenMDAODataframeCache.py b/landbosse/landbosse_omdao/OpenMDAODataframeCache.py index 4663283..06c9df7 100644 --- a/landbosse/landbosse_omdao/OpenMDAODataframeCache.py +++ b/landbosse/landbosse_omdao/OpenMDAODataframeCache.py @@ -87,6 +87,7 @@ def read_all_sheets_from_xlsx(cls, xlsx_basename, xlsx_path=None): for sheet_name in xlsx.sheet_names: sheets_dict[sheet_name].dropna(inplace=True, how='all') cls._cache[xlsx_basename] = sheets_dict + xlsx.close() return cls.copy_dataframes(sheets_dict) @classmethod diff --git a/landbosse/landbosse_omdao/landbosse.py b/landbosse/landbosse_omdao/landbosse.py index 3f5518f..834901a 100644 --- a/landbosse/landbosse_omdao/landbosse.py +++ b/landbosse/landbosse_omdao/landbosse.py @@ -31,8 +31,8 @@ def setup(self): self.set_input_defaults("turbine_spacing_rotor_diameters", 4) self.set_input_defaults("row_spacing_rotor_diameters", 10) - self.set_input_defaults("commissioning_pct", 0.01) - self.set_input_defaults("decommissioning_pct", 0.15) + self.set_input_defaults("commissioning_cost_kW", 44.0, units="USD/kW") + self.set_input_defaults("decommissioning_cost_kW", 58.0, units="USD/kW") self.set_input_defaults("trench_len_to_substation_km", 50.0, units="km") self.set_input_defaults("interconnect_voltage_kV", 130.0, units="kV") @@ -42,7 +42,7 @@ def setup(self): self.set_input_defaults("nacelle_mass", 50e3, units="kg") self.set_input_defaults("tower_mass", 240e3, units="kg") self.set_input_defaults("turbine_rating_MW", 1500.0, units="kW") - self.set_input_defaults("turbine_capex", 0.0, units="USD/kW") + self.set_input_defaults("turbine_capex_kW", 0.0, units="USD/kW") self.add_subsystem("landbosse", LandBOSSE_API(), promotes=["*"]) @@ -102,6 +102,7 @@ def setup_inputs(self): self.add_input("hub_height_meters", val=80, units="m", desc="Hub height m") self.add_input("rotor_diameter_m", val=77, units="m", desc="Rotor diameter m") self.add_input("wind_shear_exponent", val=0.2, desc="Wind shear exponent") + self.add_input("turbine_capex_kW", val=0.0, units="USD/kW", desc="Turbine capital cost") self.add_input("turbine_rating_MW", val=1.5, units="MW", desc="Turbine rating MW") self.add_input("fuel_cost_usd_per_gal", val=1.5, desc="Fuel cost USD/gal") @@ -164,9 +165,8 @@ def setup_inputs(self): # Disabled due to Pandas conflict right now. self.add_input("labor_cost_multiplier", val=1.0, desc="Labor cost multiplier") - self.add_input("commissioning_pct", 0.01) - self.add_input("decommissioning_pct", 0.15) - self.add_input("turbine_capex", 0.0, units="USD/kW") + self.add_input("commissioning_cost_kW", 44.0, units="USD/kW", desc="Commissioning cost.") + self.add_input("decommissioning_cost_kW", 58.0, units="USD/kW", desc="Decommissioning cost.") def setup_discrete_inputs_that_are_not_dataframes(self): """ @@ -450,6 +450,9 @@ def prepare_master_input_dictionary(self, inputs, discrete_inputs): discrete_inputs["num_turbines"] * inputs["turbine_rating_MW"][0] ) + # Turbine Capex + incomplete_input_dict["turbine_capex"] = float(inputs["turbine_capex_kW"][0]) + # Needed to avoid distributed wind keys incomplete_input_dict["road_distributed_wind"] = False @@ -586,24 +589,23 @@ def compute_total_bos_costs(self, costs_by_module_type_operation, master_output_ installation_per_kW = 0.0 for row in costs_by_module_type_operation: + if row["Module"] in ["TurbineCost"]: + continue bos_per_kw += row["Cost / kW"] bos_per_project += row["Cost / project"] if row["Module"] in ["ErectionCost", "FoundationCost"]: installation_per_project += row["Cost / project"] installation_per_kW += row["Cost / kW"] - commissioning_pct = inputs["commissioning_pct"] - decommissioning_pct = inputs["decommissioning_pct"] + commissioning_kW = inputs["commissioning_cost_kW"] + decommissioning_kW = inputs["decommissioning_cost_kW"] - commissioning_per_project = bos_per_project * commissioning_pct - decomissioning_per_project = bos_per_project * decommissioning_pct - commissioning_per_kW = bos_per_kw * commissioning_pct - decomissioning_per_kW = bos_per_kw * decommissioning_pct + capacity = bos_per_project / bos_per_kw - outputs["total_capex_kW"] = bos_per_kw + commissioning_per_kW + decomissioning_per_kW - outputs["total_capex"] = bos_per_project + commissioning_per_project + decomissioning_per_project outputs["bos_capex"] = bos_per_project outputs["bos_capex_kW"] = bos_per_kw + outputs["total_capex_kW"] = bos_per_kw + commissioning_kW + decommissioning_kW + outputs["total_capex"] = bos_per_project + capacity*(commissioning_kW + decommissioning_kW) outputs["installation_capex"] = installation_per_project outputs["installation_capex_kW"] = installation_per_kW diff --git a/landbosse/landbosse_runner.py b/landbosse/landbosse_runner.py index 47aee8a..f17a7a0 100644 --- a/landbosse/landbosse_runner.py +++ b/landbosse/landbosse_runner.py @@ -3,6 +3,8 @@ """ import typing +from copy import deepcopy +from pathlib import Path import attrs import pandas as pd @@ -95,34 +97,13 @@ class LandBOSSERunner: "labor_cost_multiplier": "Multiplier to modify labor costs", "crane_breakdown_fraction": "What fraction of cranes will breakdown. 0 means none, 1 means " "all. Breakdowns increase the total erection duration", - "component": { - "nacelle": { - "mass_t": "nacelle mass (t)", - "surface_area_m2": "nacelle surface area (m^2)", - }, - "hub": { - "mass_t": "hub mass (t)", - "surface_area_m2": "hub surface area (m^2)", - }, - "blade": { - "mass_t": "Blade mass (t) (one blade)", - "surface_area_m2": "Blade surface area (m^2) (one blade)", - }, - "tower_section": { - "mass_t": "List of tower section masses (t), from bottom to top. Must be the same " - "length as the other tower section attributes", - "surface_area_m2": "List of tower section surface areas (m^2), from bottom to top. " - "Must be the same legnth as the other tower section attributes", - "height_m": "List of tower section heights (m), from bottom to top. Must be the " - "same length as the other tower section attributes", - }, - }, + "user_trench_length_flag": "Flag (0 = no; 1 = yes) to indicate if the user-defined trench length should be used" } # Mapping from new parameter input names to those expected by LandBOSSE keys_rename: typing.ClassVar[dict] = { "id": "Project ID", - "datafile": "Project data file", + "data_tables": "Project data file", "construction_months": "Total project construction time (months)", "turbine_rating_MW": "Turbine rating MW", "hub_height_m": "Hub height m", @@ -172,11 +153,14 @@ class LandBOSSERunner: "DW only)", "labor_cost_multiplier": "Labor cost multiplier", "crane_breakdown_fraction": "Crane breakdown fraction", + "combined_homerun_trench_length_km": "Combined Homerun Trench Length to Substation (km)", + "user_trench_length_flag": "Flag for user-defined home run trench length (0 = no; 1 = yes)", } input_config: dict = attrs.field(converter=dict) weather: pd.DataFrame = attrs.field( - validator=attrs.validators.instance_of(pd.DataFrame), + default=None, + validator=attrs.validators.optional(attrs.validators.instance_of(pd.DataFrame)), ) result: LandBOSSEResult = attrs.field( validator=attrs.validators.instance_of(LandBOSSEResult), @@ -186,6 +170,38 @@ class LandBOSSERunner: def __attrs_post_init__(self): self.check_expected_configs_are_provided(self.input_config) + @staticmethod + def convert_excel_to_dict( + filename: str | Path, + project_id: str, + *, + enable_cost_and_scaling_modifications: bool = False, + ) -> dict: + """Convert a :py:attr:`project_id` from an Excel project list to a ``LandBOSSERunner`` + compliant dictionary. + + Parameters + ---------- + filename : str | Path + The project list file where the project's data can be found. + project_id : str + The ID for the project in the "Project ID" column of the project list. + enable_cost_and_scaling_modifications : bool, optional + Whether or not to enable the cost and scaling modifications, by default False. + + Returns + ------- + dict + The project listing data for a given :py:attr:`project_id` converted to a ``LandBOSSERunner``-compliant + format. + """ + df = pd.read_excel(filename) + data = df.loc[df["Project ID"].eq(project_id)].reset_index(drop=True).loc[0].to_dict() + keys_rename = {v: k for k, v in LandBOSSERunner.keys_rename.items()} + data = {keys_rename.get(k, k): v for k, v in data.items()} + data["enable_cost_and_scaling_modifications"] = enable_cost_and_scaling_modifications + return data + def check_expected_configs_are_provided(self, input_config: dict) -> None: """Checks to ensure all inputs required by the model have been provided. @@ -216,8 +232,12 @@ def get_project_parameters(self) -> pd.Series: ------- pd.Series Series containing input parameters for LandBOSSE with the required names + dict[str, pd.DataFrame] + Dictionary of the Excel project data with each sheet's name as a key and the + data loaded as a pandas DataFrame. """ - project_parameters_dict = self.input_config + project_parameters_dict = deepcopy(self.input_config) + data_sheets = project_parameters_dict.pop("data_tables") # Convert parameters dict to pandas Series (LandBOSSE expects a Series) project_parameters = pd.Series(project_parameters_dict, name="value") @@ -237,7 +257,7 @@ def get_project_parameters(self) -> pd.Series: project_parameters["Project ID with serial"] = project_parameters["Project ID"] - return project_parameters + return project_parameters, data_sheets def run(self) -> None: """Run the LandBOSSE model and save outputs in a LandBOSSEResult object instead of excel @@ -248,14 +268,11 @@ def run(self) -> None: LandBOSSEResult Outputs from LandBOSSE model """ - project_parameters = self.get_project_parameters() - data_sheets = project_parameters.pop("data_table") + project_parameters, data_sheets = self.get_project_parameters() - # Read WAVES weather data into expected LandBOSSE format - data_sheets["weather_window"] = self.add_header_to_weather_dataframe(self.weather) - - # Convert YAML component info into table format expected by LandBOSSE - data_sheets["components"] = self.create_component_dataframe(project_parameters, data_sheets) + # Prioritize weather profile provided at initialization over existing "weather_window" in Excel + if self.weather is not None: + data_sheets["weather_window"] = self.add_header_to_weather_dataframe(self.weather) xlsx_reader = XlsxReader() xlsx_reader.modify_project_data_and_project_list(data_sheets, project_parameters) @@ -338,99 +355,6 @@ def add_header_to_weather_dataframe(weather_window: pd.DataFrame) -> pd.DataFram ) return weather_window - @staticmethod - def create_component_dataframe( - project_parameters: pd.Series, - data_sheets: dict[str, pd.DataFrame], - ) -> pd.DataFrame: - """Convert the data from the input component data dictionary to the dataframe format - required by LandBOSSE. This includes rows for each separate blade and tower section. - - Parameters - ---------- - project_parameters : pd.Series - LandBOSSE YAML inputs - data_sheets : dict[str, pd.DataFrame] - LandBOSSE excel input tables - - Returns - ------- - pd.DataFrame - Component data in LandBOSSE format - """ - NUM_BLADES = 3 - - component_param = project_parameters["component"] - component_template = data_sheets["components"] - - component_nacelle = pd.Series(component_param["nacelle"], name="Nacelle") - component_nacelle["Component Name"] = "Nacelle" - - component_hub = pd.Series(component_param["hub"], name="Hub") - component_hub["Component Name"] = "Hub" - - component_blade = component_param["blade"] - component_blade["Component Name"] = "Blade" - - component_blade = pd.Series(component_blade) - component_blade = pd.concat( - objs={f"Blade {i+1:d}": component_blade for i in range(NUM_BLADES)}, - axis=1, - ) - - component_tower_section = pd.DataFrame(component_param["tower_section"]) - component_tower_section["Component Name"] = "Tower section" - component_tower_section = component_tower_section.set_axis( - [f"Tower section {i+1:d}" for i in range(len(component_tower_section.index))], - ) - component_tower_section["lift_height_m"] = ( - component_tower_section["height_m"].shift(-1).cumsum() - ) - component_tower_section["lever_arm_m"] = ( - component_tower_section["lift_height_m"] + component_tower_section["height_m"] / 2.0 - ) - - component_df = pd.concat( - objs=(component_nacelle, component_hub, component_blade, component_tower_section.T), - axis=1, - ).T - - hub_height = project_parameters["Hub height m"] - component_df = component_df.astype( - { - "height_m": float, - "lift_height_m": float, - "lever_arm_m": float, - } - ) - component_df = component_df.fillna( - { - "height_m": 0.0, - "lift_height_m": hub_height, - "lever_arm_m": hub_height, - } - ) - component_df = component_df.rename( - columns={ - "mass_t": "Mass tonne", - "surface_area_m2": "Surface area sq m", - "height_m": "Section height m", - "lift_height_m": "Lift height m", - "lever_arm_m": "Lever arm m", - } - ) - - component_combined = component_df.merge(component_template, on="Component Name") - component_combined = component_combined.drop(columns="Component Name") - component_combined.insert( - loc=0, - value=component_df.index, - column="Component", - ) - component_combined = component_combined.convert_dtypes() - - return component_combined - if __name__ == "__main__": pass diff --git a/landbosse/model/ErectionCost.py b/landbosse/model/ErectionCost.py index 7436847..03fe4f0 100644 --- a/landbosse/model/ErectionCost.py +++ b/landbosse/model/ErectionCost.py @@ -144,7 +144,7 @@ def __init__(self, input_dict, output_dict, project_name): material_price (pd.DatFrame) Prices for various materials used during erection. rsmeans - (p.DataFrame) RSMeans data + (pd.DataFrame) RSMeans data output_dict : dict The output dictionary with key value pairs as found on the output documentation. diff --git a/landbosse/model/FoundationCost.py b/landbosse/model/FoundationCost.py index bab37e9..bddeeed 100644 --- a/landbosse/model/FoundationCost.py +++ b/landbosse/model/FoundationCost.py @@ -158,7 +158,7 @@ def calculate_foundation_load(self, foundation_load_input_data, foundation_load_ * Section height m * Surface area sq m * Coeff drag (installed) - * Lever arm m m + * Lever arm m * Multplier drag rotor * Multiplier tower drag * Mass tonne diff --git a/pyproject.toml b/pyproject.toml index 780588a..64a86df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,27 +11,16 @@ requires-python = ">=3.10" license = { text = "Apache-2.0" } keywords = ["wind", "turbine", "mdao", "design", "optimization"] authors = [ - { name = "NREL WISDEM Team", email = "systems.engineering@nrel.gov" }, + { name = "NLR WISDEM Team", email = "systems.engineering@nlr.gov" }, ] maintainers = [ - { name = "NREL WISDEM Team", email = "systems.engineering@nrel.gov" }, + { name = "NLR WISDEM Team", email = "systems.engineering@nlr.gov" }, ] -classifiers = [ # Optional - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable +classifiers = [ "Development Status :: 4 - Beta", - - # Indicate who your project is intended for "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", - "License :: OSI Approved :: Apache Software License", - - # Specify the Python versions you support here. In particular, ensure - # that you indicate you support Python 3. These classifiers are *not* - # checked by "pip install". See instead "python_requires" below. "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -43,40 +32,21 @@ classifiers = [ # Optional "Programming Language :: Fortran", ] -dependencies = ['numpy', 'openpyxl', 'pandas', 'scipy', 'xlsxwriter'] +dependencies = ['numpy', 'openpyxl', 'pandas', 'scipy', 'xlsxwriter', 'openmdao'] -# List additional groups of dependencies here (e.g. development -# dependencies). Users will be able to install these using the "extras" -# syntax, for example: -# -# $ pip install sampleproject[dev] -# -# Similar to `dependencies` above, these must be valid existing -# projects. [project.optional-dependencies] # Optional test = ["coveralls", "pytest"] docs = ["jupyter-book==1.*", "myst-parser", "sphinxcontrib-napoleon"] +develop = ["NREL-landbosse[test, docs]"] -# List URLs that are relevant to your project -# -# This field corresponds to the "Project-URL" and "Home-Page" metadata fields: -# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use -# https://packaging.python.org/specifications/core-metadata/#home-page-optional -# -# Examples listed include a pattern for specifying where the package tracks -# issues, where the source is hosted, where to say thanks to the package -# maintainers, and where to support the project financially. The key is -# what's used to render the link text on PyPI. -[project.urls] # Optional -"Homepage" = "https://github.com/WISDEM/LandBOSSE" -#"Documentation" = "https://wisdem.readthedocs.io" -"Project" = "https://www.nrel.gov/wind/systems-engineering.html" +[project.urls] +"Homepage" = "https://github.com/NLRWIndSystems/LandBOSSE" +"Documentation" = "https://nlrwindsystems.readthedocs.io" +"Project" = "https://www.nlr.gov/wind/systems-engineering.html" #[project.scripts] # What to do about main.py program -# This is configuration specific to the `setuptools` build backend. -# If you are using a different build backend, you will need to change this. [tool.setuptools] include-package-data = true