Spaces:
Sleeping
Sleeping
[DOCS/FIX] First draft of Objective No. 5: TO FACILITATE MAINTENANCE AND UPDATES FOR CG STAFF. (No more hard-coded settings, libraries updated, instructions added to the README)
Browse files- README.md +594 -1
- app.py +127 -98
- assets/styles.css +26 -48
- config/config.py +86 -157
- data/.csv_hash +1 -0
- data/generate_input_template.py +2 -2
- data/{generate_cost_matrix.py → generate_model_data.py} +92 -27
- data/inputs_aggregated.csv +84 -84
- data/inputs_template.xlsx +0 -0
- data/method_groups.json +52 -0
- data/resource_template.xlsx +0 -0
- data/secondary_resources.csv +0 -50
- data/secondary_resources.json +226 -0
- data/tooltips.xlsx +0 -0
- data/update.py +51 -0
- data/update_cost_sliders_with_priority.py +0 -32
- optimization/optimization.py +395 -189
- requirements.txt +0 -0
- tabs/tab0_welcome.py +42 -33
- tabs/tab1_inputs.py +868 -817
- tabs/tab2_constraints.py +200 -187
- tabs/tab3_coefficients.py +509 -421
- tabs/tab4_optimization.py +0 -0
- tabs/tab5_sensitivity.py +245 -185
- tabs/tab6_scenarios.py +198 -167
- utils/data_utils.py +234 -142
- utils/ui_helpers.py +45 -1
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 🌍
|
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
| 6 |
sdk: streamlit
|
| 7 |
-
sdk_version: "1.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
|
@@ -12,3 +12,596 @@ pinned: false
|
|
| 12 |
# Carbon Dioxide Removal Optimizer
|
| 13 |
|
| 14 |
A Streamlit app for optimizing carbon removal portfolios across different methods and constraints.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
| 6 |
sdk: streamlit
|
| 7 |
+
sdk_version: "1.58.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
|
|
|
| 12 |
# Carbon Dioxide Removal Optimizer
|
| 13 |
|
| 14 |
A Streamlit app for optimizing carbon removal portfolios across different methods and constraints.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## Data Maintenance Guide
|
| 19 |
+
|
| 20 |
+
This guide is for Carbon Gap staff who need to update the tool's data — **no coding experience required** for most tasks.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
### How it works (overview)
|
| 25 |
+
|
| 26 |
+
All the data the tool uses (CDR methods, resources, coefficients) comes from a single file:
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
data/inputs_aggregated.csv
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Every time the app starts, it computes a fingerprint (hash) of the CSV and compares it to the last known value. If they differ — meaning the CSV has been modified — it regenerates all data files automatically before loading. You do not need to run anything manually.
|
| 33 |
+
|
| 34 |
+
There are two ways to update the data, depending on whether you are working directly on Hugging Face or on a local copy of the project.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
### Option 1 — Edit directly on Hugging Face (no local setup needed)
|
| 39 |
+
|
| 40 |
+
This is the simplest option for non-coders with admin access to the Hugging Face Space.
|
| 41 |
+
|
| 42 |
+
**Steps:**
|
| 43 |
+
|
| 44 |
+
1. Go to the Hugging Face Space → click the **Files** tab
|
| 45 |
+
2. Open `data/inputs_aggregated.csv` and click **Edit** (pencil icon), or upload a new version of the file
|
| 46 |
+
3. Commit the change directly on Hugging Face — the Space will restart automatically
|
| 47 |
+
4. **Check the logs** to confirm the update ran:
|
| 48 |
+
- In the Space, click **⋮ (three dots)** → **Logs**
|
| 49 |
+
- You should see:
|
| 50 |
+
```
|
| 51 |
+
inputs_aggregated.csv has changed — running data/update.py ...
|
| 52 |
+
[... update details ...]
|
| 53 |
+
Data files updated successfully.
|
| 54 |
+
```
|
| 55 |
+
5. If the logs show this message, the app is running with the new data. If not, something went wrong — contact the developer.
|
| 56 |
+
|
| 57 |
+
> **Note:** with this option, only `inputs_aggregated.csv` needs to be uploaded. The app handles the rest automatically on restart.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
### Option 2 — Work locally and push (recommended for larger changes)
|
| 62 |
+
|
| 63 |
+
Use this option when making bigger changes (new methods, new resources, reordering) where you want to preview the result before publishing.
|
| 64 |
+
|
| 65 |
+
**Step 1 — Get the project on your machine**
|
| 66 |
+
|
| 67 |
+
If you do not already have it:
|
| 68 |
+
- Install [GitHub Desktop](https://desktop.github.com) (free, no command line needed)
|
| 69 |
+
- Open GitHub Desktop → **File** → **Clone repository** → enter the repository URL → choose a local folder → click **Clone**
|
| 70 |
+
|
| 71 |
+
**Step 2 — Edit the CSV**
|
| 72 |
+
|
| 73 |
+
Open `data/inputs_aggregated.csv` in Excel or any spreadsheet editor. Make your changes and save.
|
| 74 |
+
|
| 75 |
+
**Step 3 — Open a terminal in the project folder**
|
| 76 |
+
|
| 77 |
+
- Mac: right-click the project folder → **New Terminal at Folder**
|
| 78 |
+
- Windows: open the folder, click in the address bar, type `cmd`, press Enter
|
| 79 |
+
|
| 80 |
+
Activate the Python environment:
|
| 81 |
+
```
|
| 82 |
+
source venv/bin/activate # Mac / Linux
|
| 83 |
+
venv\Scripts\activate # Windows
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
**Step 4 — Run the update script**
|
| 87 |
+
|
| 88 |
+
```
|
| 89 |
+
python data/update.py
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
This regenerates all data files and updates the internal fingerprint. Check the output — it should end with `All files updated successfully.`
|
| 93 |
+
|
| 94 |
+
**Step 5 — Commit and push**
|
| 95 |
+
|
| 96 |
+
In GitHub Desktop:
|
| 97 |
+
- You will see all modified files listed on the left
|
| 98 |
+
- Write a short description in the **Summary** field (e.g. "Update coefficients for Afforestation")
|
| 99 |
+
- Click **Commit to main**
|
| 100 |
+
- Click **Push origin**
|
| 101 |
+
|
| 102 |
+
Hugging Face will detect the push and rebuild the Space automatically.
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
### Quick reference — where is each parameter?
|
| 107 |
+
|
| 108 |
+
| I want to change… | File | Where exactly |
|
| 109 |
+
|---|---|---|
|
| 110 |
+
| A coefficient value (min / median / max) | `data/inputs_aggregated.csv` | Columns `min`, `median`, `max` — find the row by `Implementation` + `Resource` |
|
| 111 |
+
| The name of a CDR method | `data/inputs_aggregated.csv` | Column `Implementation` — use Find & Replace |
|
| 112 |
+
| Which group a CDR method belongs to | `data/inputs_aggregated.csv` | Column `Method group` |
|
| 113 |
+
| The name of a resource | `data/inputs_aggregated.csv` | Column `Resource` — use Find & Replace |
|
| 114 |
+
| Which group a resource belongs to | `data/inputs_aggregated.csv` | Column `Resource group` |
|
| 115 |
+
| The unit of a resource group | `data/inputs_aggregated.csv` | Column `Unit` — must be consistent across all rows for that group |
|
| 116 |
+
| Whether a resource is optional for a method | `data/inputs_aggregated.csv` | Column `Is secondary` — `TRUE` = optional, `FALSE` = always active |
|
| 117 |
+
| The display order of methods or resources | `data/inputs_aggregated.csv` | Row order — move rows up/down |
|
| 118 |
+
| The app's colours | `config/config.py` | `CRRA_COLORS` dictionary |
|
| 119 |
+
| The app's font | `config/config.py` | `CRRA_FONT` variable |
|
| 120 |
+
| The tooltip text on a method name | `data/tooltips.xlsx` | Sheet **Methods** — column `Tooltip` |
|
| 121 |
+
| The tooltip text on a resource name | `data/tooltips.xlsx` | Sheet **Resources** — column `Tooltip` |
|
| 122 |
+
| UI spacing, font sizes, button styles | `assets/styles.css` | Standard CSS — search for the element name |
|
| 123 |
+
| Python library versions | `requirements.txt` | One line per library — see Library Maintenance Guide below |
|
| 124 |
+
|
| 125 |
+
> After any change to `inputs_aggregated.csv`, run `python data/update.py`. After changes to `config/config.py` or `assets/styles.css`, simply restart the app.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
### Before you start
|
| 130 |
+
|
| 131 |
+
**Open a terminal** in the project folder. On Mac: right-click the folder → "New Terminal at Folder". On Windows: open the folder, then type `cmd` in the address bar.
|
| 132 |
+
|
| 133 |
+
**Activate the Python environment** (only needed once per session):
|
| 134 |
+
|
| 135 |
+
- Mac / Linux: `source venv/bin/activate`
|
| 136 |
+
- Windows: `venv\Scripts\activate`
|
| 137 |
+
|
| 138 |
+
You should now see `(venv)` at the start of your terminal line.
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
### File structure (what each file does)
|
| 143 |
+
|
| 144 |
+
| File | What it is | Do you edit it? |
|
| 145 |
+
|------|-----------|-----------------|
|
| 146 |
+
| `data/inputs_aggregated.csv` | **Main data file** — all methods, resources and coefficients | ✅ Yes |
|
| 147 |
+
| `data/update.py` | Script that regenerates everything from the CSV | ❌ No |
|
| 148 |
+
| `data/cost_sliders.json` | Auto-generated — coefficients used by the app | ❌ No (auto) |
|
| 149 |
+
| `data/method_groups.json` | Auto-generated — CDR method categories | ❌ No (auto) |
|
| 150 |
+
| `data/secondary_resources.json` | Auto-generated — optional resource assignments | ❌ No (auto) |
|
| 151 |
+
| `data/inputs_template.xlsx` | Auto-generated — Excel template for users | ❌ No (auto) |
|
| 152 |
+
| `data/resource_template.xlsx` | Auto-generated — Excel template for resources | ❌ No (auto) |
|
| 153 |
+
| `data/tooltips.xlsx` | Tooltip texts shown on hover in the app | ✅ Yes — edit directly |
|
| 154 |
+
|
| 155 |
+
> ⚠️ **Never edit the auto-generated files directly** — your changes will be overwritten the next time `update.py` is run.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
### Understanding `inputs_aggregated.csv`
|
| 160 |
+
|
| 161 |
+
Each row represents **one resource used by one CDR method**, with the following columns:
|
| 162 |
+
|
| 163 |
+
| Column | Description | Example |
|
| 164 |
+
|--------|-------------|---------|
|
| 165 |
+
| `Implementation` | Name of the CDR method | `Afforestation` |
|
| 166 |
+
| `Method group` | Category the method belongs to | `Forestry` |
|
| 167 |
+
| `Resource` | Name of the resource used | `Unspecified land` |
|
| 168 |
+
| `Resource group` | Category the resource belongs to | `Land` |
|
| 169 |
+
| `Type` | Physical type of the resource | `Physical` |
|
| 170 |
+
| `Unit` | Unit of the coefficient (per MtCO₂) | `Mha/MtCO2` |
|
| 171 |
+
| `min` | Minimum coefficient value | `0.002` |
|
| 172 |
+
| `median` | Default (median) coefficient value | `0.094927` |
|
| 173 |
+
| `max` | Maximum coefficient value | `0.384615` |
|
| 174 |
+
| `Is secondary` | Whether this resource is optional for this method | `FALSE` |
|
| 175 |
+
|
| 176 |
+
> **Primary resources** (`Is secondary = FALSE`) are always active in the model.
|
| 177 |
+
> **Secondary resources** (`Is secondary = TRUE`) are optional — users can toggle them on in the app.
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## Case 1 — Modify existing coefficient values
|
| 182 |
+
|
| 183 |
+
**When to use:** The literature has been updated and you want to change the min/median/max values for an existing method-resource pair.
|
| 184 |
+
|
| 185 |
+
**Steps:**
|
| 186 |
+
|
| 187 |
+
1. Open `data/inputs_aggregated.csv` in Excel or a text editor.
|
| 188 |
+
2. Find the row with the correct `Implementation` and `Resource`.
|
| 189 |
+
3. Edit the `min`, `median`, and/or `max` columns.
|
| 190 |
+
4. Save the file.
|
| 191 |
+
5. In the terminal, run:
|
| 192 |
+
```
|
| 193 |
+
python data/update.py
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## Case 2 — Add a new resource to an existing method
|
| 199 |
+
|
| 200 |
+
**When to use:** A CDR method now uses a resource that wasn't tracked before (e.g., adding water consumption to Afforestation).
|
| 201 |
+
|
| 202 |
+
**Steps:**
|
| 203 |
+
|
| 204 |
+
1. Open `data/inputs_aggregated.csv`.
|
| 205 |
+
2. Add a **new row** at the end (or anywhere in the file — order of rows determines display order in the app):
|
| 206 |
+
|
| 207 |
+
| Column | What to enter |
|
| 208 |
+
|--------|--------------|
|
| 209 |
+
| `Implementation` | Exact name of the existing method (copy it from another row to avoid typos) |
|
| 210 |
+
| `Method group` | Same group as the other rows for that method |
|
| 211 |
+
| `Resource` | Name of the resource (must match an existing resource exactly, or see Case 4 to add a new one) |
|
| 212 |
+
| `Resource group` | Same group as the other rows for that resource |
|
| 213 |
+
| `Type` | `Physical` or `Energy` |
|
| 214 |
+
| `Unit` | Must match the unit already used for that resource (e.g., `Mha/MtCO2`) |
|
| 215 |
+
| `min` | Minimum coefficient |
|
| 216 |
+
| `median` | Median coefficient |
|
| 217 |
+
| `max` | Maximum coefficient |
|
| 218 |
+
| `Is secondary` | `TRUE` if optional, `FALSE` if always required |
|
| 219 |
+
|
| 220 |
+
3. Save the file.
|
| 221 |
+
4. Run `python data/update.py`.
|
| 222 |
+
|
| 223 |
+
---
|
| 224 |
+
|
| 225 |
+
## Case 3 — Add a new CDR method
|
| 226 |
+
|
| 227 |
+
**When to use:** A new carbon removal approach needs to be added to the tool.
|
| 228 |
+
|
| 229 |
+
**Steps:**
|
| 230 |
+
|
| 231 |
+
1. Open `data/inputs_aggregated.csv`.
|
| 232 |
+
2. Add **one row per resource** used by the new method. For example, if your new method uses land and water, add 2 rows.
|
| 233 |
+
|
| 234 |
+
For each row, fill in all columns. Use an **existing Method group** (e.g., `Forestry`) or create a new one (the name you write here becomes the category name in the app).
|
| 235 |
+
|
| 236 |
+
3. Save the file.
|
| 237 |
+
4. Run `python data/update.py`.
|
| 238 |
+
|
| 239 |
+
> **Note on display order:** The new method will appear in the app in the same position it appears in the CSV. If you want it between two existing methods, insert the rows between those methods' rows.
|
| 240 |
+
|
| 241 |
+
> ⚠️ **Special case — derived methods:** see Case 3b below.
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
## Case 3b — Add a derived CDR method (variant of an existing method)
|
| 246 |
+
|
| 247 |
+
**What is a derived method?**
|
| 248 |
+
Some methods in the tool are not standalone entries in the CSV — they are automatically generated as **variants** of a base method. For example:
|
| 249 |
+
- `Bio-char` in the CSV → generates 3 variants in the app: `Bio-char - Forest biomass`, `Bio-char - Non-forest biomass`, `Bio-char - Unspecified biomass`
|
| 250 |
+
- `Enhanced weathering` in the CSV → generates 4 variants: one per mineral type
|
| 251 |
+
|
| 252 |
+
Each variant is identical to the base method **except** it only keeps the resource relevant to that variant (e.g. `Bio-char - Forest biomass` removes the non-forest and unspecified biomass resources).
|
| 253 |
+
|
| 254 |
+
**When to use:** You want to add a new variant of an existing method (e.g. a new mineral type for Enhanced weathering, or a new biomass type for Bio-char).
|
| 255 |
+
|
| 256 |
+
**Steps:**
|
| 257 |
+
|
| 258 |
+
1. Open `data/inputs_aggregated.csv` and make sure the base method has a row for the new resource (e.g. add a row for `Enhanced weathering` with the new mineral).
|
| 259 |
+
|
| 260 |
+
2. Open `data/generate_model_data.py`. Find the two structures at the top of the file:
|
| 261 |
+
|
| 262 |
+
**`REPLACED_BY_DERIVED`** — lists base methods that are fully replaced by their variants (the base name disappears from the app):
|
| 263 |
+
```
|
| 264 |
+
REPLACED_BY_DERIVED = {"Bio-char", "Enhanced weathering"}
|
| 265 |
+
```
|
| 266 |
+
If your new base method should disappear from the app once variants exist, add its name here.
|
| 267 |
+
|
| 268 |
+
**`DERIVED_FROM`** — maps each base method to its list of variant names (in display order):
|
| 269 |
+
```
|
| 270 |
+
DERIVED_FROM = {
|
| 271 |
+
"Bio-char": ["Bio-char - Forest biomass", ...],
|
| 272 |
+
"Enhanced weathering": ["Enhanced weathering - Basalt mineral", ...],
|
| 273 |
+
...
|
| 274 |
+
}
|
| 275 |
+
```
|
| 276 |
+
Add the new variant name to the relevant list here.
|
| 277 |
+
|
| 278 |
+
3. Further down in `create_sliders_inputs()`, add the derivation logic for your new variant — copy the pattern already used for Bio-char or Enhanced weathering: copy the base method's coefficients, then remove the resources that don't apply to this variant.
|
| 279 |
+
|
| 280 |
+
4. Save `generate_model_data.py` and run:
|
| 281 |
+
```
|
| 282 |
+
python data/update.py
|
| 283 |
+
```
|
| 284 |
+
|
| 285 |
+
> ⚠️ This case requires editing Python code. If you are not comfortable with this, contact the developer — provide the name of the new variant and which resource it should keep, and they can add the 5–10 lines needed.
|
| 286 |
+
|
| 287 |
+
---
|
| 288 |
+
|
| 289 |
+
## Case 4 — Add a new resource
|
| 290 |
+
|
| 291 |
+
**When to use:** A resource that doesn't exist yet needs to be tracked (e.g., a new type of mineral or energy).
|
| 292 |
+
|
| 293 |
+
**Steps:**
|
| 294 |
+
|
| 295 |
+
1. Decide which **Resource group** the new resource belongs to (e.g., `Minerals`, `Energy`, `Land`). If it belongs to an existing group, use the exact same group name. If it needs its own group, invent a new name.
|
| 296 |
+
|
| 297 |
+
2. Open `data/inputs_aggregated.csv` and add one row per method that uses this resource (same as Case 2).
|
| 298 |
+
|
| 299 |
+
3. **Check the unit:** the unit for all resources in a group must be the same (e.g., all `Land` resources use `Mha/MtCO2`). If your new resource has a different unit, it needs its own Resource group.
|
| 300 |
+
|
| 301 |
+
4. Save the file and run `python data/update.py`.
|
| 302 |
+
|
| 303 |
+
---
|
| 304 |
+
|
| 305 |
+
## Case 5 — Add a new resource group (new category)
|
| 306 |
+
|
| 307 |
+
**When to use:** Your new resource doesn't fit any existing category.
|
| 308 |
+
|
| 309 |
+
**Steps:**
|
| 310 |
+
|
| 311 |
+
Same as Case 4, but in the `Resource group` column, write a **new group name** that has never been used before. The app will automatically create a new section for it in the Resource Inputs tab.
|
| 312 |
+
|
| 313 |
+
The new group will appear in the app **in the position of its first row in the CSV**. Move the rows up or down to control where it appears.
|
| 314 |
+
|
| 315 |
+
---
|
| 316 |
+
|
| 317 |
+
## Case 6 — Change whether a resource is optional (secondary) for a method
|
| 318 |
+
|
| 319 |
+
**When to use:** You want to make a resource optional (user can enable/disable it) or make it always required.
|
| 320 |
+
|
| 321 |
+
**Steps:**
|
| 322 |
+
|
| 323 |
+
1. Open `data/inputs_aggregated.csv`.
|
| 324 |
+
2. Find the row with the correct `Implementation` and `Resource`.
|
| 325 |
+
3. Change the `Is secondary` column to `TRUE` (optional) or `FALSE` (always active).
|
| 326 |
+
4. Save and run `python data/update.py`.
|
| 327 |
+
|
| 328 |
+
---
|
| 329 |
+
|
| 330 |
+
## Case 7 — Rename a method or resource
|
| 331 |
+
|
| 332 |
+
**When to use:** Correcting a spelling mistake or updating nomenclature.
|
| 333 |
+
|
| 334 |
+
**Steps:**
|
| 335 |
+
|
| 336 |
+
1. Open `data/inputs_aggregated.csv`.
|
| 337 |
+
2. Use **Find & Replace** (Ctrl+H / Cmd+H) to replace the old name with the new name everywhere in the file. Make sure to replace it in both the `Implementation` column (for methods) or `Resource` column (for resources), and also in the `Method group` / `Resource group` columns if the group name changed.
|
| 338 |
+
3. Save and run `python data/update.py`.
|
| 339 |
+
|
| 340 |
+
> ⚠️ If users have saved scenario files (`.xlsx` or `.zip`) that reference the old name, those files will no longer load correctly after the rename.
|
| 341 |
+
|
| 342 |
+
---
|
| 343 |
+
|
| 344 |
+
## Case 8 — Edit tooltip texts (hover descriptions)
|
| 345 |
+
|
| 346 |
+
**When to use:** You want to add, update, or remove the short description that appears when a user hovers over a resource or method name in the app.
|
| 347 |
+
|
| 348 |
+
**Steps:**
|
| 349 |
+
|
| 350 |
+
1. Open `data/tooltips.xlsx`.
|
| 351 |
+
2. Go to the relevant sheet:
|
| 352 |
+
- **Resources** — for the hover text on resource names (Tab 1 "Resource Inputs")
|
| 353 |
+
- **Methods** — for the hover text on method names (Tab 2 "Method Constraints")
|
| 354 |
+
3. Find the row with the name you want to edit. All resource and method names are already listed — you only need to fill in or change the `Tooltip` column.
|
| 355 |
+
4. To **add** a tooltip: type the text in the `Tooltip` cell for that row.
|
| 356 |
+
5. To **remove** a tooltip: leave the `Tooltip` cell empty.
|
| 357 |
+
6. Save the file.
|
| 358 |
+
7. Restart the app (no need to run `update.py`).
|
| 359 |
+
|
| 360 |
+
> ⚠️ The name in column `Resource` or `Method` must match **exactly** the name used in `inputs_aggregated.csv` (same spelling, same capitalisation). If you rename a method or resource in the CSV, remember to update the name in `tooltips.xlsx` too.
|
| 361 |
+
|
| 362 |
+
---
|
| 363 |
+
|
| 364 |
+
## Case 10 — Change the display order of methods or resources
|
| 365 |
+
|
| 366 |
+
**When to use:** You want a method or resource group to appear earlier or later in the app.
|
| 367 |
+
|
| 368 |
+
**Steps:**
|
| 369 |
+
|
| 370 |
+
1. Open `data/inputs_aggregated.csv`.
|
| 371 |
+
2. Move the relevant rows up or down. The app displays methods and resource groups **in the same order as they appear in the CSV**.
|
| 372 |
+
3. Save and run `python data/update.py`.
|
| 373 |
+
|
| 374 |
+
---
|
| 375 |
+
|
| 376 |
+
## After any change — checklist
|
| 377 |
+
|
| 378 |
+
**Option 1 (Hugging Face direct):**
|
| 379 |
+
- [ ] Edited and committed `inputs_aggregated.csv` on Hugging Face
|
| 380 |
+
- [ ] Checked the Space logs — confirmed `Data files updated successfully.`
|
| 381 |
+
- [ ] Verified the change appears correctly in the app
|
| 382 |
+
|
| 383 |
+
**Option 2 (local):**
|
| 384 |
+
- [ ] Saved `inputs_aggregated.csv`
|
| 385 |
+
- [ ] Ran `python data/update.py` with no errors
|
| 386 |
+
- [ ] Committed all modified files (CSV, JSON files, templates) and pushed
|
| 387 |
+
- [ ] Verified the change appears correctly in the app after the Space rebuilt
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
## Common mistakes
|
| 392 |
+
|
| 393 |
+
| Mistake | What happens | Fix |
|
| 394 |
+
|---------|-------------|-----|
|
| 395 |
+
| Typo in `Method group` or `Resource group` | A new accidental group is created in the app | Fix the typo and re-run `update.py` |
|
| 396 |
+
| Wrong unit (e.g., `MWh/MtCO2` instead of `Mha/MtCO2`) | The resource appears in the wrong group or breaks the unit display | Correct the `Unit` column |
|
| 397 |
+
| Edited a JSON file directly | Changes are overwritten next time `update.py` runs | Always edit the CSV, never the JSON |
|
| 398 |
+
| Forgot to run `update.py` | The app still shows the old data | Run `python data/update.py` |
|
| 399 |
+
| `min` value equals `max` value | The model automatically halves the `min` to create a range | Intentional for fixed-value resources like Geologic storage — leave as is |
|
| 400 |
+
|
| 401 |
+
---
|
| 402 |
+
|
| 403 |
+
## Library Maintenance Guide
|
| 404 |
+
|
| 405 |
+
This section explains how to keep the tool's software dependencies up to date — safely and without breaking anything.
|
| 406 |
+
|
| 407 |
+
---
|
| 408 |
+
|
| 409 |
+
### What is a library (dependency)?
|
| 410 |
+
|
| 411 |
+
The tool is built on top of external software packages ("libraries") that handle things like displaying the interface, reading Excel files, or running the optimization. These libraries are listed in `requirements.txt` with their exact version numbers (e.g., `streamlit==1.45.1`).
|
| 412 |
+
|
| 413 |
+
Libraries sometimes release updates — fixing bugs, patching security holes, or adding new features. The risk is that an update can also change how a library behaves and break the tool.
|
| 414 |
+
|
| 415 |
+
---
|
| 416 |
+
|
| 417 |
+
### The key libraries and what they do
|
| 418 |
+
|
| 419 |
+
These are the libraries that matter most for this tool:
|
| 420 |
+
|
| 421 |
+
| Library | Role | Risk if updated |
|
| 422 |
+
|---------|------|----------------|
|
| 423 |
+
| **streamlit** | Powers the entire web interface (tabs, sliders, buttons, charts) — currently 1.58.0 | 🔴 High — UI layout and widget behavior can change |
|
| 424 |
+
| **streamlit-option-menu** | The navigation sidebar menu | 🟡 Medium — must stay compatible with streamlit version |
|
| 425 |
+
| **pandas** | Reads and processes CSV and Excel files | 🟡 Medium — occasional API changes |
|
| 426 |
+
| **numpy** | Numerical operations (arrays, math) | 🟡 Medium — used by scipy and pandas |
|
| 427 |
+
| **scipy** | Runs the LP optimization (`linprog`) | 🟡 Medium — optimization results could change |
|
| 428 |
+
| **plotly** | All charts and interactive graphs | 🟡 Medium — chart API evolves |
|
| 429 |
+
| **openpyxl** | Reads and writes Excel files (.xlsx) | 🟢 Low — stable API |
|
| 430 |
+
| **pillow** | Image handling (indirect use) | 🟢 Low |
|
| 431 |
+
| **requests / urllib3 / certifi** | Network and security | 🟢 Low — update freely for security patches |
|
| 432 |
+
|
| 433 |
+
---
|
| 434 |
+
|
| 435 |
+
### Dependency chain (what depends on what)
|
| 436 |
+
|
| 437 |
+
Some libraries require specific versions of others to work. Updating one can force updates to others:
|
| 438 |
+
|
| 439 |
+
```
|
| 440 |
+
streamlit ──────────────► tornado (web server)
|
| 441 |
+
├───────────► altair (charts)
|
| 442 |
+
├───────────► pyarrow (data performance)
|
| 443 |
+
├───────────► narwhals (data compatibility)
|
| 444 |
+
├───────────► protobuf
|
| 445 |
+
└───────────► cachetools, blinker, click, toml
|
| 446 |
+
|
| 447 |
+
pandas ─────────────────► numpy
|
| 448 |
+
├───────────────► pyarrow
|
| 449 |
+
└───────────────► python-dateutil, pytz, tzdata
|
| 450 |
+
|
| 451 |
+
scipy ──────────────────► numpy
|
| 452 |
+
|
| 453 |
+
plotly ─────────────────► narwhals
|
| 454 |
+
|
| 455 |
+
openpyxl ──────────────► pillow
|
| 456 |
+
```
|
| 457 |
+
|
| 458 |
+
**Practical implication:** if you update `streamlit`, you may also need to update `tornado`, `altair`, `pyarrow`, etc. If you update `pandas`, you may need to update `numpy`. These cascading updates are handled automatically by pip (see below), but they can introduce unexpected changes.
|
| 459 |
+
|
| 460 |
+
---
|
| 461 |
+
|
| 462 |
+
### When to update
|
| 463 |
+
|
| 464 |
+
#### ✅ Update immediately
|
| 465 |
+
- **Security vulnerabilities** in `requests`, `urllib3`, `certifi`, or `pillow` — these are reported publicly and should be patched fast.
|
| 466 |
+
- Check https://pypi.org/project/[library-name] or subscribe to GitHub security alerts.
|
| 467 |
+
|
| 468 |
+
#### ✅ Update when needed
|
| 469 |
+
- **Streamlit** — when a new feature in the app requires it, or when a critical bug in the current version is causing problems.
|
| 470 |
+
- **pandas / numpy / scipy** — when a bug in the current version is causing incorrect data processing or optimization results.
|
| 471 |
+
- **openpyxl** — when Excel import/export breaks with newer `.xlsx` files.
|
| 472 |
+
|
| 473 |
+
#### ❌ Do NOT update just because a newer version exists
|
| 474 |
+
- "Latest is always better" is not true for production tools. An update that works fine in isolation can break interactions between libraries.
|
| 475 |
+
- The tool has been tested with the versions in `requirements.txt`. Changing them without testing is risky.
|
| 476 |
+
|
| 477 |
+
---
|
| 478 |
+
|
| 479 |
+
### How to safely update a library
|
| 480 |
+
|
| 481 |
+
**Never update all libraries at once.** Update one at a time, test the tool after each one.
|
| 482 |
+
|
| 483 |
+
**Step 1 — Back up the current working state**
|
| 484 |
+
|
| 485 |
+
```
|
| 486 |
+
cp requirements.txt requirements.txt.backup
|
| 487 |
+
```
|
| 488 |
+
|
| 489 |
+
**Step 2 — Simulate first (dry run)**
|
| 490 |
+
|
| 491 |
+
Before applying anything, check whether the update would cause conflicts:
|
| 492 |
+
```
|
| 493 |
+
pip install --upgrade streamlit --dry-run
|
| 494 |
+
```
|
| 495 |
+
If pip reports conflicts, investigate before proceeding.
|
| 496 |
+
|
| 497 |
+
**Step 3 — Update the library**
|
| 498 |
+
|
| 499 |
+
```
|
| 500 |
+
pip install --upgrade streamlit
|
| 501 |
+
```
|
| 502 |
+
|
| 503 |
+
Or to a specific version:
|
| 504 |
+
```
|
| 505 |
+
pip install streamlit==1.58.0
|
| 506 |
+
```
|
| 507 |
+
|
| 508 |
+
**Step 4 — Verify no dependencies are broken**
|
| 509 |
+
|
| 510 |
+
```
|
| 511 |
+
pip check
|
| 512 |
+
```
|
| 513 |
+
This confirms that all installed packages are mutually compatible. If it reports any conflict, do not proceed to the next step — restore the backup instead (see Step 6).
|
| 514 |
+
|
| 515 |
+
**Step 5 — Freeze the new versions to requirements.txt**
|
| 516 |
+
|
| 517 |
+
```
|
| 518 |
+
pip freeze > requirements.txt
|
| 519 |
+
```
|
| 520 |
+
|
| 521 |
+
**Step 6 — Test the tool**
|
| 522 |
+
|
| 523 |
+
- Start the app: `streamlit run app.py`
|
| 524 |
+
- Go through every tab
|
| 525 |
+
- Import and export an Excel file
|
| 526 |
+
- Run an optimization
|
| 527 |
+
- Check that charts display correctly
|
| 528 |
+
|
| 529 |
+
**Step 7 — If something breaks**
|
| 530 |
+
|
| 531 |
+
Restore the backup:
|
| 532 |
+
```
|
| 533 |
+
pip install -r requirements.txt.backup
|
| 534 |
+
cp requirements.txt.backup requirements.txt
|
| 535 |
+
```
|
| 536 |
+
|
| 537 |
+
---
|
| 538 |
+
|
| 539 |
+
### How to install all libraries on a new machine
|
| 540 |
+
|
| 541 |
+
```
|
| 542 |
+
python -m venv venv
|
| 543 |
+
source venv/bin/activate # Mac/Linux
|
| 544 |
+
# or: venv\Scripts\activate # Windows
|
| 545 |
+
pip install -r requirements.txt
|
| 546 |
+
```
|
| 547 |
+
|
| 548 |
+
---
|
| 549 |
+
|
| 550 |
+
### Checking for outdated libraries
|
| 551 |
+
|
| 552 |
+
To see which installed libraries have newer versions available:
|
| 553 |
+
|
| 554 |
+
```
|
| 555 |
+
pip list --outdated
|
| 556 |
+
```
|
| 557 |
+
|
| 558 |
+
This shows a list with current and latest versions. Use this to **identify** what could be updated — but don't update everything at once.
|
| 559 |
+
|
| 560 |
+
---
|
| 561 |
+
|
| 562 |
+
### Checking for known security vulnerabilities
|
| 563 |
+
|
| 564 |
+
Install the security scanner:
|
| 565 |
+
```
|
| 566 |
+
pip install pip-audit
|
| 567 |
+
```
|
| 568 |
+
|
| 569 |
+
Run it:
|
| 570 |
+
```
|
| 571 |
+
pip-audit
|
| 572 |
+
```
|
| 573 |
+
|
| 574 |
+
This will flag any library with a known security issue and suggest a fix. These should be addressed promptly.
|
| 575 |
+
|
| 576 |
+
> **Recommended frequency:** run `pip-audit` at least once a year, even if the tool appears stable. Security vulnerabilities are discovered independently of whether the library has changed — a version that was safe last year may be flagged today.
|
| 577 |
+
|
| 578 |
+
---
|
| 579 |
+
|
| 580 |
+
### Using an AI assistant to handle updates
|
| 581 |
+
|
| 582 |
+
Library updates sometimes require code changes alongside the pip update — for example, a deprecated function that was renamed, or a new argument that became required. Doing this manually requires reading changelogs, finding every affected line, and testing the result.
|
| 583 |
+
|
| 584 |
+
An AI coding assistant such as **Claude Code** (available at [claude.ai/code](https://claude.ai/code)) can handle this end-to-end:
|
| 585 |
+
|
| 586 |
+
- Run `pip-audit` or `pip list --outdated` and paste the output into Claude Code
|
| 587 |
+
- Describe the update you want to make (e.g., "update pandas and fix any deprecation warnings")
|
| 588 |
+
- Claude Code will update the library, search the codebase for affected code, apply the necessary fixes, and update `requirements.txt`
|
| 589 |
+
|
| 590 |
+
This is especially useful when:
|
| 591 |
+
- A deprecation warning points to a function that is used in multiple places across the codebase
|
| 592 |
+
- A major version bump changes how a library behaves (e.g., pandas 3.x changing its default string backend)
|
| 593 |
+
- You are not comfortable navigating the code yourself
|
| 594 |
+
|
| 595 |
+
> The AI handles the mechanical parts; you remain responsible for testing the result with `streamlit run app.py` before committing and pushing.
|
| 596 |
+
|
| 597 |
+
---
|
| 598 |
+
|
| 599 |
+
### Summary: update strategy
|
| 600 |
+
|
| 601 |
+
```
|
| 602 |
+
Security fix needed? ──► Update immediately (requests, urllib3, certifi, pillow)
|
| 603 |
+
Something is broken? ──► Update only the relevant library, test thoroughly
|
| 604 |
+
Nothing is broken? ──► Do not update
|
| 605 |
+
```
|
| 606 |
+
|
| 607 |
+
---
|
app.py
CHANGED
|
@@ -1,99 +1,128 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
from
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
tab6_scenarios.render_tab()
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import subprocess
|
| 5 |
+
import pathlib
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from streamlit_option_menu import option_menu
|
| 9 |
+
|
| 10 |
+
# ── Auto-update: regenerate JSON files if inputs_aggregated.csv has changed ───
|
| 11 |
+
# Uses a hash file to detect changes — works both locally and on Hugging Face.
|
| 12 |
+
# Must run before importing config/tabs, which read the generated JSON files.
|
| 13 |
+
_csv = "data/inputs_aggregated.csv"
|
| 14 |
+
_hash_file = "data/.csv_hash"
|
| 15 |
+
_generated = [
|
| 16 |
+
"data/cost_sliders.json",
|
| 17 |
+
"data/method_groups.json",
|
| 18 |
+
"data/secondary_resources.json",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
def _md5(path):
|
| 22 |
+
hasher = hashlib.md5()
|
| 23 |
+
with open(path, "rb") as f:
|
| 24 |
+
hasher.update(f.read())
|
| 25 |
+
return hasher.hexdigest()
|
| 26 |
+
|
| 27 |
+
if os.path.exists(_csv):
|
| 28 |
+
_current_hash = _md5(_csv)
|
| 29 |
+
_stored_hash = open(_hash_file).read().strip() if os.path.exists(_hash_file) else ""
|
| 30 |
+
_missing = any(not os.path.exists(f) for f in _generated)
|
| 31 |
+
|
| 32 |
+
if _current_hash != _stored_hash or _missing:
|
| 33 |
+
print("inputs_aggregated.csv has changed — running data/update.py ...")
|
| 34 |
+
subprocess.run([sys.executable, "data/update.py"], check=True)
|
| 35 |
+
with open(_hash_file, "w") as _f:
|
| 36 |
+
_f.write(_current_hash)
|
| 37 |
+
print("Data files updated successfully.")
|
| 38 |
+
|
| 39 |
+
from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
|
| 40 |
+
tab3_coefficients, tab4_optimization, tab5_sensitivity,
|
| 41 |
+
tab6_scenarios)
|
| 42 |
+
from utils.data_utils import load_slider_data, strip_unit_suffix
|
| 43 |
+
from config.config import method_order, secondary_resources
|
| 44 |
+
|
| 45 |
+
st.set_page_config("Carbon Portfolio Optimizer", layout="wide")
|
| 46 |
+
|
| 47 |
+
def load_css(file_path):
|
| 48 |
+
with open(file_path) as f:
|
| 49 |
+
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
|
| 50 |
+
|
| 51 |
+
css_path = pathlib.Path("assets/styles.css")
|
| 52 |
+
load_css(css_path)
|
| 53 |
+
|
| 54 |
+
st.markdown('<h1 class="custom-title">CRRA <span class="crra-hl">Toolkit</span>: CDR Portfolio Optimization</h1>', unsafe_allow_html=True)
|
| 55 |
+
|
| 56 |
+
with st.sidebar:
|
| 57 |
+
selected = option_menu(
|
| 58 |
+
"Navigation",
|
| 59 |
+
options=[
|
| 60 |
+
"Welcome",
|
| 61 |
+
"Resource inputs",
|
| 62 |
+
"Method constraints",
|
| 63 |
+
"Resource coefficients",
|
| 64 |
+
"Portfolio generation",
|
| 65 |
+
"Sensitivity analysis",
|
| 66 |
+
"Scenario comparison",
|
| 67 |
+
],
|
| 68 |
+
icons=["house", "upload", "gear", "sliders", "graph-up-arrow", "card-checklist", "search"],
|
| 69 |
+
default_index=0,
|
| 70 |
+
orientation="vertical",
|
| 71 |
+
key="crra-menu",
|
| 72 |
+
styles={
|
| 73 |
+
"container":{
|
| 74 |
+
"border-radius":"0px",
|
| 75 |
+
},
|
| 76 |
+
"menu-title":{
|
| 77 |
+
"font-family": "Segoe UI, sans-serif",
|
| 78 |
+
"font-weight":"bold",
|
| 79 |
+
"color": "#ff5f4d",
|
| 80 |
+
"text-transform": "uppercase",
|
| 81 |
+
"font-style": "italic",
|
| 82 |
+
},
|
| 83 |
+
"nav-link": {
|
| 84 |
+
"font-family": "Segoe UI, sans-serif",
|
| 85 |
+
"margin": "4px 0",
|
| 86 |
+
"--hover-color": "#F5F5F5",
|
| 87 |
+
"text-transform": "uppercase",
|
| 88 |
+
"font-weight":"600",
|
| 89 |
+
"font-style": "italic",
|
| 90 |
+
},
|
| 91 |
+
"nav-link-selected": {
|
| 92 |
+
"background-color": "#eae9d8",
|
| 93 |
+
"color": "#298231",
|
| 94 |
+
"font-weight": "900",
|
| 95 |
+
},
|
| 96 |
+
}
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
if "constraints" not in st.session_state:
|
| 100 |
+
st.session_state.constraints = {}
|
| 101 |
+
for m in method_order:
|
| 102 |
+
if m not in st.session_state.constraints:
|
| 103 |
+
st.session_state.constraints[m] = {"active": True, "cap_type": "percent", "cap_value": 100}
|
| 104 |
+
|
| 105 |
+
cost_sliders = load_slider_data()
|
| 106 |
+
if "costs" not in st.session_state or st.session_state.costs == {}:
|
| 107 |
+
st.session_state.costs = {
|
| 108 |
+
m: {
|
| 109 |
+
strip_unit_suffix(r): (0.0 if (m, strip_unit_suffix(r)) in secondary_resources else cost_sliders[m][r]["median"])
|
| 110 |
+
for r in cost_sliders[m]
|
| 111 |
+
}
|
| 112 |
+
for m in cost_sliders
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
if selected == "Welcome":
|
| 116 |
+
tab0_welcome.render_tab()
|
| 117 |
+
elif selected == "Resource inputs":
|
| 118 |
+
tab1_inputs.render_tab(cost_sliders)
|
| 119 |
+
elif selected == "Method constraints":
|
| 120 |
+
tab2_constraints.render_tab()
|
| 121 |
+
elif selected == "Resource coefficients":
|
| 122 |
+
tab3_coefficients.render_tab(cost_sliders)
|
| 123 |
+
elif selected == "Portfolio generation":
|
| 124 |
+
tab4_optimization.render_tab()
|
| 125 |
+
elif selected == "Sensitivity analysis":
|
| 126 |
+
tab5_sensitivity.render_tab()
|
| 127 |
+
elif selected == "Scenario comparison":
|
| 128 |
tab6_scenarios.render_tab()
|
assets/styles.css
CHANGED
|
@@ -182,7 +182,8 @@ h1.custom-title {
|
|
| 182 |
[data-testid="stAlert"] > div,
|
| 183 |
[data-testid="stAlert"] > div > div {
|
| 184 |
border-radius: 0px !important;
|
| 185 |
-
clip-path: polygon(1% 0, 100% 0, 99% 100%, 0 100%);
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
[data-testid="stAlert"] p {
|
|
@@ -190,7 +191,7 @@ h1.custom-title {
|
|
| 190 |
font-size: 0.95rem;
|
| 191 |
line-height: 1.5;
|
| 192 |
letter-spacing: 0.01em;
|
| 193 |
-
padding-left:
|
| 194 |
}
|
| 195 |
|
| 196 |
/* Special warning box (tab1) */
|
|
@@ -206,8 +207,8 @@ h1.custom-title {
|
|
| 206 |
|
| 207 |
.st-key-warning_resources span,
|
| 208 |
[data-testid="stAlert"] span {
|
| 209 |
-
font-style: italic;
|
| 210 |
-
padding-left:
|
| 211 |
}
|
| 212 |
|
| 213 |
/* ── 8. Metrics ────────────────────────────────────────────── */
|
|
@@ -393,20 +394,8 @@ h1.custom-title {
|
|
| 393 |
}
|
| 394 |
|
| 395 |
/* Text & number input fields */
|
| 396 |
-
[data-testid="
|
| 397 |
-
|
| 398 |
-
border-radius: 0px !important;
|
| 399 |
-
border-top: none !important;
|
| 400 |
-
border-left: none !important;
|
| 401 |
-
border-right: none !important;
|
| 402 |
-
background: transparent !important;
|
| 403 |
-
box-shadow: none !important;
|
| 404 |
-
transition: border-color 0.2s ease;
|
| 405 |
-
}
|
| 406 |
-
[data-testid="stTextInput"] input:focus,
|
| 407 |
-
[data-testid="stNumberInput"] input:focus {
|
| 408 |
-
border-bottom-color: var(--green-dark) !important;
|
| 409 |
-
box-shadow: none !important;
|
| 410 |
}
|
| 411 |
|
| 412 |
/* Text area */
|
|
@@ -476,13 +465,6 @@ section[data-testid="stExpander"] summary p {
|
|
| 476 |
}
|
| 477 |
|
| 478 |
/* ── 12. File uploaders ─────────────────────────────────────── */
|
| 479 |
-
[class*="resource_uploader_"],
|
| 480 |
-
[class*="inputs_loader_"],
|
| 481 |
-
[class*="scenario_loader_"],
|
| 482 |
-
.st-key-comparison_loader {
|
| 483 |
-
font-family: var(--font);
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
[class*="resource_uploader_"] p,
|
| 487 |
[class*="inputs_loader_"] p,
|
| 488 |
[class*="scenario_loader_"] p,
|
|
@@ -491,35 +473,30 @@ section[data-testid="stExpander"] summary p {
|
|
| 491 |
font-style: italic;
|
| 492 |
}
|
| 493 |
|
| 494 |
-
[
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
.st-key-comparison_loader [data-testid="stFileUploaderDropzone"] {
|
| 498 |
-
background-color: var(--off-white);
|
| 499 |
-
color: var(--green-dark);
|
| 500 |
-
font-weight: 700;
|
| 501 |
-
letter-spacing: 0.094444em;
|
| 502 |
-
text-transform: uppercase;
|
| 503 |
-
font-size: 0.875rem;
|
| 504 |
-
border-radius: 0px !important;
|
| 505 |
-
width: stretch;
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
[class*="resource_uploader_"] [data-testid="stFileUploaderDropzone"] span svg,
|
| 509 |
-
[class*="inputs_loader_"] [data-testid="stFileUploaderDropzone"] span svg,
|
| 510 |
-
[class*="scenario_loader_"] [data-testid="stFileUploaderDropzone"] span svg,
|
| 511 |
-
.st-key-comparison_loader [data-testid="stFileUploaderDropzone"] span svg {
|
| 512 |
-
color: var(--green-dark);
|
| 513 |
}
|
| 514 |
|
| 515 |
[class*="resource_uploader_"] [data-testid="stBaseButton-secondary"],
|
| 516 |
[class*="inputs_loader_"] [data-testid="stBaseButton-secondary"],
|
| 517 |
[class*="scenario_loader_"] [data-testid="stBaseButton-secondary"] {
|
| 518 |
-
color: var(--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
font-weight: 700 !important;
|
| 520 |
text-transform: uppercase;
|
| 521 |
-
font-style:
|
| 522 |
-
background-color: var(--green-dark) !important;
|
| 523 |
border: none !important;
|
| 524 |
}
|
| 525 |
|
|
@@ -594,10 +571,11 @@ section[data-testid="stExpander"] summary p {
|
|
| 594 |
gap: 1.125%;
|
| 595 |
}
|
| 596 |
|
| 597 |
-
|
| 598 |
display: flex;
|
| 599 |
flex-direction: row-reverse;
|
| 600 |
gap: 1.125%;
|
|
|
|
| 601 |
}
|
| 602 |
|
| 603 |
/* ── 17. Scrollbar ──────────────────────────────────────────── */
|
|
|
|
| 182 |
[data-testid="stAlert"] > div,
|
| 183 |
[data-testid="stAlert"] > div > div {
|
| 184 |
border-radius: 0px !important;
|
| 185 |
+
/* clip-path: polygon(1% 0, 100% 0, 99% 100%, 0 100%); */
|
| 186 |
+
clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%)
|
| 187 |
}
|
| 188 |
|
| 189 |
[data-testid="stAlert"] p {
|
|
|
|
| 191 |
font-size: 0.95rem;
|
| 192 |
line-height: 1.5;
|
| 193 |
letter-spacing: 0.01em;
|
| 194 |
+
padding-left: 1rem;
|
| 195 |
}
|
| 196 |
|
| 197 |
/* Special warning box (tab1) */
|
|
|
|
| 207 |
|
| 208 |
.st-key-warning_resources span,
|
| 209 |
[data-testid="stAlert"] span {
|
| 210 |
+
/* font-style: italic; */
|
| 211 |
+
padding-left: 1rem;
|
| 212 |
}
|
| 213 |
|
| 214 |
/* ── 8. Metrics ────────────────────────────────────────────── */
|
|
|
|
| 394 |
}
|
| 395 |
|
| 396 |
/* Text & number input fields */
|
| 397 |
+
[data-testid="stNumberInputContainer"] {
|
| 398 |
+
background-color: var(--off-white);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
/* Text area */
|
|
|
|
| 465 |
}
|
| 466 |
|
| 467 |
/* ── 12. File uploaders ─────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
[class*="resource_uploader_"] p,
|
| 469 |
[class*="inputs_loader_"] p,
|
| 470 |
[class*="scenario_loader_"] p,
|
|
|
|
| 473 |
font-style: italic;
|
| 474 |
}
|
| 475 |
|
| 476 |
+
[data-testid="stFileUploaderDropzone"]{
|
| 477 |
+
align-items: center;
|
| 478 |
+
flex-direction: row-reverse;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
}
|
| 480 |
|
| 481 |
[class*="resource_uploader_"] [data-testid="stBaseButton-secondary"],
|
| 482 |
[class*="inputs_loader_"] [data-testid="stBaseButton-secondary"],
|
| 483 |
[class*="scenario_loader_"] [data-testid="stBaseButton-secondary"] {
|
| 484 |
+
color: var(--green-dark) !important;
|
| 485 |
+
border: none !important;
|
| 486 |
+
border-radius: 0.5rem;
|
| 487 |
+
}
|
| 488 |
+
[class*="resource_uploader_"] [data-testid="stBaseButton-secondary"]:hover,
|
| 489 |
+
[class*="inputs_loader_"] [data-testid="stBaseButton-secondary"]:hover,
|
| 490 |
+
[class*="scenario_loader_"] [data-testid="stBaseButton-secondary"]:hover {
|
| 491 |
+
background-color: var(--green-light);
|
| 492 |
+
}
|
| 493 |
+
[class*="resource_uploader_"] [data-testid="stBaseButton-secondary"] p,
|
| 494 |
+
[class*="inputs_loader_"] [data-testid="stBaseButton-secondary"] p ,
|
| 495 |
+
[class*="scenario_loader_"] [data-testid="stBaseButton-secondary"] p {
|
| 496 |
+
color: var(--green-dark) !important;
|
| 497 |
font-weight: 700 !important;
|
| 498 |
text-transform: uppercase;
|
| 499 |
+
font-style: normal;
|
|
|
|
| 500 |
border: none !important;
|
| 501 |
}
|
| 502 |
|
|
|
|
| 571 |
gap: 1.125%;
|
| 572 |
}
|
| 573 |
|
| 574 |
+
[class*="st-key-container-button"] {
|
| 575 |
display: flex;
|
| 576 |
flex-direction: row-reverse;
|
| 577 |
gap: 1.125%;
|
| 578 |
+
justify-content: flex-start;
|
| 579 |
}
|
| 580 |
|
| 581 |
/* ── 17. Scrollbar ──────────────────────────────────────────── */
|
config/config.py
CHANGED
|
@@ -1,157 +1,86 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
"
|
| 16 |
-
|
| 17 |
-
]
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
]
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
"
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# "Wollastonite mineral": "Wollastonite mineral used in enhanced weathering.",
|
| 88 |
-
# "Unspecified mineral": "General mineral not specified for enhanced weathering.",
|
| 89 |
-
# "Unspecified chemical": "General chemical resource not specified.",
|
| 90 |
-
# "Salt water": "Salt water used in ocean-based methods.",
|
| 91 |
-
# "Unspecified water": "General water resource not specified.",
|
| 92 |
-
# "Shoreline": "Length of shoreline affected by coastal methods."
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
method_tooltips = {
|
| 96 |
-
"Afforestation": "Methods related to afforestation practices.",
|
| 97 |
-
"Reforestation": "Methods related to reforestation practices.",
|
| 98 |
-
"Forest management": "Sustainable management of forest resources.",
|
| 99 |
-
"Agroforestry": "Integration of trees and shrubs into agricultural systems.",
|
| 100 |
-
"Cropland management": "Practices to enhance carbon storage in croplands.",
|
| 101 |
-
"Pasture management": "Sustainable practices for managing pastures.",
|
| 102 |
-
"Coastal revegetation (mangrove, seaweeds)": "Restoration of coastal ecosystems using mangroves and seaweeds.",
|
| 103 |
-
"Durable biobased products": "Production of long-lasting biobased materials.",
|
| 104 |
-
"Bio-char - Forest biomass": "Production of bio-char from forest biomass.",
|
| 105 |
-
"Bio-char - Non-forest biomass": "Production of bio-char from non-forest biomass.",
|
| 106 |
-
"Bio-char - Unspecified biomass": "Production of bio-char from unspecified biomass sources.",
|
| 107 |
-
"BioCCS with combustion": "Bioenergy with carbon capture and storage using combustion.",
|
| 108 |
-
"BioCCS with combustion & growing biomass": "Bioenergy with carbon capture and storage using combustion and growing biomass.",
|
| 109 |
-
"BioCCS with anaerobic digestion/fermentation": "Bioenergy with carbon capture and storage using anaerobic digestion or fermentation.",
|
| 110 |
-
"BioCCS with anaerobic digestion/fermentation & growing biomass": "Bioenergy with carbon capture and storage using anaerobic digestion or fermentation and growing biomass.",
|
| 111 |
-
"Enhanced weathering - Basalt mineral": "Use of basalt mineral for enhanced weathering.",
|
| 112 |
-
"Enhanced weathering - Olivine mineral": "Use of olivine mineral for enhanced weathering.",
|
| 113 |
-
"Enhanced weathering - Wollastonite mineral": "Use of wollastonite mineral for enhanced weathering.",
|
| 114 |
-
"Enhanced weathering - Unspecified mineral": "Use of unspecified mineral for enhanced weathering.",
|
| 115 |
-
"Mineral OAE / Ocean liming": "Ocean alkalinity enhancement using mineral or ocean liming.",
|
| 116 |
-
"Electrochemical OAE": "Ocean alkalinity enhancement using electrochemical methods.",
|
| 117 |
-
"Electrochemical DACCS": "Direct air carbon capture and storage using electrochemical methods.",
|
| 118 |
-
"Low-temp DACCS (solid-sorbent/liquid-solvent)": "Direct air carbon capture and storage using low-temperature solid sorbents or liquid solvents.",
|
| 119 |
-
"High-temp DACCS (solid-sorbent/liquid-solvent)": "Direct air carbon capture and storage using high-temperature solid sorbents or liquid solvents.",
|
| 120 |
-
"Mineral looping DACCS": "Direct air carbon capture and storage using mineral looping.",
|
| 121 |
-
"Moisture swing DACCS": "Direct air carbon capture and storage using moisture swing methods.",
|
| 122 |
-
"pH swing": "Direct ocean carbon capture and storage using pH swing methods.",
|
| 123 |
-
"Mineralization ex-situ": "Ex-situ mineralization methods for carbon capture."
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
CRRA_COLORS = {
|
| 127 |
-
"off_white": "#eae9d8",
|
| 128 |
-
"red": "#ff5f4d",
|
| 129 |
-
"green_dark": "#298231",
|
| 130 |
-
"green": "#55c95b",
|
| 131 |
-
"green_light": "#92ef9b",
|
| 132 |
-
"yellow": "#d9e210",
|
| 133 |
-
"white": "#ffffff",
|
| 134 |
-
"brown_grey": "#665d5b",
|
| 135 |
-
"grey": "#a0a0a0",
|
| 136 |
-
"black": "#000000",
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
CRRA_FONT = "Segoe UI, sans-serif"
|
| 140 |
-
|
| 141 |
-
PLOTLY_COLOR_SEQUENCE = [
|
| 142 |
-
CRRA_COLORS["red"],
|
| 143 |
-
CRRA_COLORS["green"],
|
| 144 |
-
CRRA_COLORS["yellow"],
|
| 145 |
-
CRRA_COLORS["green_dark"],
|
| 146 |
-
CRRA_COLORS["brown_grey"],
|
| 147 |
-
CRRA_COLORS["green_light"],
|
| 148 |
-
CRRA_COLORS["grey"],
|
| 149 |
-
]
|
| 150 |
-
|
| 151 |
-
PLOTLY_HEATMAP_SCALE = [
|
| 152 |
-
[0.0, CRRA_COLORS["white"]],
|
| 153 |
-
[0.5, CRRA_COLORS["green_light"]],
|
| 154 |
-
[1.0, CRRA_COLORS["green_dark"]],
|
| 155 |
-
]
|
| 156 |
-
|
| 157 |
-
PLOTLY_TEMPLATE = "plotly_white"
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
|
| 5 |
+
CO2_UNIT = "MtCO₂"
|
| 6 |
+
|
| 7 |
+
with open("data/method_groups.json") as _f:
|
| 8 |
+
method_groups: dict[str, list[str]] = json.load(_f)
|
| 9 |
+
|
| 10 |
+
_csv = pd.read_csv("data/inputs_aggregated.csv")
|
| 11 |
+
|
| 12 |
+
# resource_groups: group → [resources], order follows first appearance in CSV
|
| 13 |
+
resource_groups: dict[str, list[str]] = {}
|
| 14 |
+
for _, _row in _csv.drop_duplicates(subset=["Resource group", "Resource"]).iterrows():
|
| 15 |
+
_g, _r = _row["Resource group"], _row["Resource"]
|
| 16 |
+
resource_groups.setdefault(_g, [])
|
| 17 |
+
if _r not in resource_groups[_g]:
|
| 18 |
+
resource_groups[_g].append(_r)
|
| 19 |
+
|
| 20 |
+
# resource_unit_map: group → base unit (e.g. "Mha/MtCO2" → "Mha")
|
| 21 |
+
resource_unit_map: dict[str, str] = {}
|
| 22 |
+
for _, _row in _csv.drop_duplicates(subset=["Resource group"]).iterrows():
|
| 23 |
+
resource_unit_map[_row["Resource group"]] = str(_row["Unit"]).split("/")[0]
|
| 24 |
+
|
| 25 |
+
resource_order = sum(resource_groups.values(), [])
|
| 26 |
+
method_order = sum(method_groups.values(), [])
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
with open("data/secondary_resources.json") as _f:
|
| 30 |
+
secondary_resources: set[tuple[str, str]] = {tuple(p) for p in json.load(_f)}
|
| 31 |
+
except FileNotFoundError:
|
| 32 |
+
secondary_resources: set[tuple[str, str]] = set()
|
| 33 |
+
|
| 34 |
+
# Tooltips are loaded from data/tooltips.xlsx (two sheets: "Resources" and "Methods").
|
| 35 |
+
# To add or edit a tooltip, open that file — no code change needed.
|
| 36 |
+
# If the file is missing, tooltips are silently disabled (no hover text shown).
|
| 37 |
+
try:
|
| 38 |
+
_tooltips_xl = pd.read_excel("data/tooltips.xlsx", sheet_name=None)
|
| 39 |
+
_df_res = _tooltips_xl.get("Resources", pd.DataFrame(columns=["Resource", "Tooltip"]))
|
| 40 |
+
_df_met = _tooltips_xl.get("Methods", pd.DataFrame(columns=["Method", "Tooltip"]))
|
| 41 |
+
resource_tooltips: dict[str, str] = {
|
| 42 |
+
row["Resource"]: row["Tooltip"]
|
| 43 |
+
for _, row in _df_res.iterrows()
|
| 44 |
+
if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
|
| 45 |
+
}
|
| 46 |
+
method_tooltips: dict[str, str] = {
|
| 47 |
+
row["Method"]: row["Tooltip"]
|
| 48 |
+
for _, row in _df_met.iterrows()
|
| 49 |
+
if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
|
| 50 |
+
}
|
| 51 |
+
except FileNotFoundError:
|
| 52 |
+
resource_tooltips: dict[str, str] = {}
|
| 53 |
+
method_tooltips: dict[str, str] = {}
|
| 54 |
+
|
| 55 |
+
CRRA_COLORS = {
|
| 56 |
+
"off_white": "#eae9d8",
|
| 57 |
+
"red": "#ff5f4d",
|
| 58 |
+
"green_dark": "#298231",
|
| 59 |
+
"green": "#55c95b",
|
| 60 |
+
"green_light": "#92ef9b",
|
| 61 |
+
"yellow": "#d9e210",
|
| 62 |
+
"white": "#ffffff",
|
| 63 |
+
"brown_grey": "#665d5b",
|
| 64 |
+
"grey": "#a0a0a0",
|
| 65 |
+
"black": "#000000",
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
CRRA_FONT = "Segoe UI, sans-serif"
|
| 69 |
+
|
| 70 |
+
PLOTLY_COLOR_SEQUENCE = [
|
| 71 |
+
CRRA_COLORS["red"],
|
| 72 |
+
CRRA_COLORS["green"],
|
| 73 |
+
CRRA_COLORS["yellow"],
|
| 74 |
+
CRRA_COLORS["green_dark"],
|
| 75 |
+
CRRA_COLORS["brown_grey"],
|
| 76 |
+
CRRA_COLORS["green_light"],
|
| 77 |
+
CRRA_COLORS["grey"],
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
PLOTLY_HEATMAP_SCALE = [
|
| 81 |
+
[0.0, CRRA_COLORS["white"]],
|
| 82 |
+
[0.5, CRRA_COLORS["green_light"]],
|
| 83 |
+
[1.0, CRRA_COLORS["green_dark"]],
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
PLOTLY_TEMPLATE = "plotly_white"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/.csv_hash
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ae18d57252350286638943fd58c0300c
|
data/generate_input_template.py
CHANGED
|
@@ -14,8 +14,8 @@ from utils.data_utils import strip_unit_suffix
|
|
| 14 |
with open("data/cost_sliders.json") as f:
|
| 15 |
cost_sliders = json.load(f)
|
| 16 |
|
| 17 |
-
|
| 18 |
-
secondary_resources =
|
| 19 |
|
| 20 |
wb = openpyxl.Workbook()
|
| 21 |
|
|
|
|
| 14 |
with open("data/cost_sliders.json") as f:
|
| 15 |
cost_sliders = json.load(f)
|
| 16 |
|
| 17 |
+
with open("data/secondary_resources.json") as _f:
|
| 18 |
+
secondary_resources = {tuple(p) for p in json.load(_f)}
|
| 19 |
|
| 20 |
wb = openpyxl.Workbook()
|
| 21 |
|
data/{generate_cost_matrix.py → generate_model_data.py}
RENAMED
|
@@ -14,24 +14,83 @@ def pop_by_name(d: dict, name: str):
|
|
| 14 |
if key:
|
| 15 |
d.pop(key)
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
def create_sliders_inputs():
|
| 18 |
df = pd.read_csv("data/inputs_aggregated.csv")
|
| 19 |
|
| 20 |
cost_sliders = {}
|
| 21 |
|
| 22 |
-
#
|
|
|
|
| 23 |
for _, row in df.iterrows():
|
| 24 |
method = row["Implementation"]
|
| 25 |
-
# break bio-char into 3 different cases, based on which biomass is used
|
| 26 |
-
"""
|
| 27 |
-
For the Implementation "Biochar", we must create three different cases:
|
| 28 |
-
1. Bio-char - Forest biomass
|
| 29 |
-
2. Bio-char - Non-forest biomass
|
| 30 |
-
3. Bio-char - Unspecified biomass
|
| 31 |
-
Where we have to keep the value for the corresponding resource in:
|
| 32 |
-
[Forest biomass OR Non-forest biomass OR Unspecified biomass]
|
| 33 |
-
But set the other two to zero, and keep the values in the other resources intact
|
| 34 |
-
"""
|
| 35 |
resource = row["Resource"] + " (" + row["Unit"] + ")"
|
| 36 |
if row["min"] == row["max"] and row["Resource"] != "Geologic storage":
|
| 37 |
row["min"] = row["min"] * .5
|
|
@@ -41,10 +100,9 @@ def create_sliders_inputs():
|
|
| 41 |
"max": float(row["max"])
|
| 42 |
}
|
| 43 |
cost_sliders.setdefault(method, {})[resource] = entry
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# create the three cases for bio-char
|
| 48 |
cost_sliders["Bio-char - Forest biomass"] = cost_sliders["Bio-char"].copy()
|
| 49 |
pop_by_name(cost_sliders["Bio-char - Forest biomass"], "Non-forest biomass")
|
| 50 |
pop_by_name(cost_sliders["Bio-char - Forest biomass"], "Unspecified biomass")
|
|
@@ -54,11 +112,9 @@ def create_sliders_inputs():
|
|
| 54 |
cost_sliders["Bio-char - Unspecified biomass"] = cost_sliders["Bio-char"].copy()
|
| 55 |
pop_by_name(cost_sliders["Bio-char - Unspecified biomass"], "Forest biomass")
|
| 56 |
pop_by_name(cost_sliders["Bio-char - Unspecified biomass"], "Non-forest biomass")
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# create all cases for enhanced weathering
|
| 62 |
cost_sliders["Enhanced weathering - Basalt mineral"] = cost_sliders["Enhanced weathering"].copy()
|
| 63 |
pop_by_name(cost_sliders["Enhanced weathering - Basalt mineral"], "Olivine mineral")
|
| 64 |
pop_by_name(cost_sliders["Enhanced weathering - Basalt mineral"], "Wollastonite mineral")
|
|
@@ -75,24 +131,33 @@ def create_sliders_inputs():
|
|
| 75 |
pop_by_name(cost_sliders["Enhanced weathering - Unspecified mineral"], "Basalt mineral")
|
| 76 |
pop_by_name(cost_sliders["Enhanced weathering - Unspecified mineral"], "Olivine mineral")
|
| 77 |
pop_by_name(cost_sliders["Enhanced weathering - Unspecified mineral"], "Wollastonite mineral")
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# create case for growing biomass in bioccs
|
| 82 |
cost_sliders["BioCCS with combustion & growing biomass"] = cost_sliders["BioCCS with combustion"].copy()
|
| 83 |
pop_by_name(cost_sliders["BioCCS with combustion"], "Arable land")
|
| 84 |
pop_by_name(cost_sliders["BioCCS with combustion & growing biomass"], "Forest biomass")
|
| 85 |
pop_by_name(cost_sliders["BioCCS with combustion & growing biomass"], "Unspecified biomass")
|
| 86 |
-
|
| 87 |
cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"] = cost_sliders["BioCCS with anaerobic digestion/fermentation"].copy()
|
| 88 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation"], "Arable land")
|
| 89 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Non-forest biomass")
|
| 90 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Unspecified biomass")
|
| 91 |
-
|
|
|
|
| 92 |
with open("data/cost_sliders.json", "w") as f:
|
| 93 |
json.dump(cost_sliders, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
if __name__ == "__main__":
|
| 98 |
create_sliders_inputs()
|
|
|
|
| 14 |
if key:
|
| 15 |
d.pop(key)
|
| 16 |
|
| 17 |
+
# Which base methods are replaced by derived variants.
|
| 18 |
+
# Methods listed here must have derived entries added in create_sliders_inputs().
|
| 19 |
+
REPLACED_BY_DERIVED = {"Bio-char", "Enhanced weathering"}
|
| 20 |
+
|
| 21 |
+
# Which derived methods follow each base method (in order).
|
| 22 |
+
DERIVED_FROM = {
|
| 23 |
+
"Bio-char": [
|
| 24 |
+
"Bio-char - Forest biomass",
|
| 25 |
+
"Bio-char - Non-forest biomass",
|
| 26 |
+
"Bio-char - Unspecified biomass",
|
| 27 |
+
],
|
| 28 |
+
"Enhanced weathering": [
|
| 29 |
+
"Enhanced weathering - Basalt mineral",
|
| 30 |
+
"Enhanced weathering - Olivine mineral",
|
| 31 |
+
"Enhanced weathering - Wollastonite mineral",
|
| 32 |
+
"Enhanced weathering - Unspecified mineral",
|
| 33 |
+
],
|
| 34 |
+
"BioCCS with combustion": ["BioCCS with combustion & growing biomass"],
|
| 35 |
+
"BioCCS with anaerobic digestion/fermentation": [
|
| 36 |
+
"BioCCS with anaerobic digestion/fermentation & growing biomass"
|
| 37 |
+
],
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_method_groups(df, cost_sliders):
|
| 42 |
+
"""Build the method_groups dict from the CSV Method group column + derivation rules above.
|
| 43 |
+
Group and method order follows first appearance in inputs_aggregated.csv."""
|
| 44 |
+
base_methods_per_group = {}
|
| 45 |
+
for _, row in df.drop_duplicates(subset=["Implementation"]).iterrows():
|
| 46 |
+
m, g = row["Implementation"], row["Method group"]
|
| 47 |
+
base_methods_per_group.setdefault(g, [])
|
| 48 |
+
if m not in base_methods_per_group[g]:
|
| 49 |
+
base_methods_per_group[g].append(m)
|
| 50 |
+
|
| 51 |
+
method_groups = {}
|
| 52 |
+
for g, base_list in base_methods_per_group.items():
|
| 53 |
+
methods = []
|
| 54 |
+
for base in base_list:
|
| 55 |
+
if base not in REPLACED_BY_DERIVED:
|
| 56 |
+
methods.append(base)
|
| 57 |
+
for derived in DERIVED_FROM.get(base, []):
|
| 58 |
+
if derived in cost_sliders:
|
| 59 |
+
methods.append(derived)
|
| 60 |
+
if methods:
|
| 61 |
+
method_groups[g] = methods
|
| 62 |
+
return method_groups
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def build_secondary_resources(df, cost_sliders):
|
| 66 |
+
"""Build the full set of (method, resource) secondary pairs from the CSV
|
| 67 |
+
'Is secondary' column, then propagate to derived methods via DERIVED_FROM."""
|
| 68 |
+
# Base pairs from CSV
|
| 69 |
+
base_secondary = set()
|
| 70 |
+
for _, row in df[df["Is secondary"] == True].iterrows():
|
| 71 |
+
base_secondary.add((row["Implementation"], row["Resource"]))
|
| 72 |
+
|
| 73 |
+
# Expand to derived methods: derived methods inherit secondary resources from their base
|
| 74 |
+
full_secondary = set(base_secondary)
|
| 75 |
+
for base, deriveds in DERIVED_FROM.items():
|
| 76 |
+
base_resources_secondary = {r for (m, r) in base_secondary if m == base}
|
| 77 |
+
for derived in deriveds:
|
| 78 |
+
if derived in cost_sliders:
|
| 79 |
+
for r in base_resources_secondary:
|
| 80 |
+
full_secondary.add((derived, r))
|
| 81 |
+
|
| 82 |
+
return full_secondary
|
| 83 |
+
|
| 84 |
+
|
| 85 |
def create_sliders_inputs():
|
| 86 |
df = pd.read_csv("data/inputs_aggregated.csv")
|
| 87 |
|
| 88 |
cost_sliders = {}
|
| 89 |
|
| 90 |
+
# Build cost_sliders from CSV: one entry per (method, resource) row.
|
| 91 |
+
# Bio-char, Enhanced weathering and BioCCS variants are derived below.
|
| 92 |
for _, row in df.iterrows():
|
| 93 |
method = row["Implementation"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
resource = row["Resource"] + " (" + row["Unit"] + ")"
|
| 95 |
if row["min"] == row["max"] and row["Resource"] != "Geologic storage":
|
| 96 |
row["min"] = row["min"] * .5
|
|
|
|
| 100 |
"max": float(row["max"])
|
| 101 |
}
|
| 102 |
cost_sliders.setdefault(method, {})[resource] = entry
|
| 103 |
+
|
| 104 |
+
# ── Derived methods ───────────────────────────────────────────────────────
|
| 105 |
+
# Bio-char: split into 3 biomass-specific variants, remove generic base
|
|
|
|
| 106 |
cost_sliders["Bio-char - Forest biomass"] = cost_sliders["Bio-char"].copy()
|
| 107 |
pop_by_name(cost_sliders["Bio-char - Forest biomass"], "Non-forest biomass")
|
| 108 |
pop_by_name(cost_sliders["Bio-char - Forest biomass"], "Unspecified biomass")
|
|
|
|
| 112 |
cost_sliders["Bio-char - Unspecified biomass"] = cost_sliders["Bio-char"].copy()
|
| 113 |
pop_by_name(cost_sliders["Bio-char - Unspecified biomass"], "Forest biomass")
|
| 114 |
pop_by_name(cost_sliders["Bio-char - Unspecified biomass"], "Non-forest biomass")
|
| 115 |
+
del cost_sliders["Bio-char"]
|
| 116 |
+
|
| 117 |
+
# Enhanced weathering: split into 4 mineral-specific variants, remove generic base
|
|
|
|
|
|
|
| 118 |
cost_sliders["Enhanced weathering - Basalt mineral"] = cost_sliders["Enhanced weathering"].copy()
|
| 119 |
pop_by_name(cost_sliders["Enhanced weathering - Basalt mineral"], "Olivine mineral")
|
| 120 |
pop_by_name(cost_sliders["Enhanced weathering - Basalt mineral"], "Wollastonite mineral")
|
|
|
|
| 131 |
pop_by_name(cost_sliders["Enhanced weathering - Unspecified mineral"], "Basalt mineral")
|
| 132 |
pop_by_name(cost_sliders["Enhanced weathering - Unspecified mineral"], "Olivine mineral")
|
| 133 |
pop_by_name(cost_sliders["Enhanced weathering - Unspecified mineral"], "Wollastonite mineral")
|
| 134 |
+
del cost_sliders["Enhanced weathering"]
|
| 135 |
+
|
| 136 |
+
# BioCCS: add growing-biomass variants alongside their base methods
|
|
|
|
| 137 |
cost_sliders["BioCCS with combustion & growing biomass"] = cost_sliders["BioCCS with combustion"].copy()
|
| 138 |
pop_by_name(cost_sliders["BioCCS with combustion"], "Arable land")
|
| 139 |
pop_by_name(cost_sliders["BioCCS with combustion & growing biomass"], "Forest biomass")
|
| 140 |
pop_by_name(cost_sliders["BioCCS with combustion & growing biomass"], "Unspecified biomass")
|
| 141 |
+
|
| 142 |
cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"] = cost_sliders["BioCCS with anaerobic digestion/fermentation"].copy()
|
| 143 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation"], "Arable land")
|
| 144 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Non-forest biomass")
|
| 145 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Unspecified biomass")
|
| 146 |
+
|
| 147 |
+
# ── Save outputs ──────────────────────────────────────────────────────────
|
| 148 |
with open("data/cost_sliders.json", "w") as f:
|
| 149 |
json.dump(cost_sliders, f, indent=2)
|
| 150 |
+
print("Saved cost_sliders.json")
|
| 151 |
+
|
| 152 |
+
method_groups = build_method_groups(df, cost_sliders)
|
| 153 |
+
with open("data/method_groups.json", "w") as f:
|
| 154 |
+
json.dump(method_groups, f, indent=2)
|
| 155 |
+
print("Saved method_groups.json")
|
| 156 |
|
| 157 |
+
secondary_resources = build_secondary_resources(df, cost_sliders)
|
| 158 |
+
with open("data/secondary_resources.json", "w") as f:
|
| 159 |
+
json.dump(sorted(secondary_resources), f, indent=2)
|
| 160 |
+
print(f"Saved secondary_resources.json ({len(secondary_resources)} pairs)")
|
| 161 |
|
| 162 |
if __name__ == "__main__":
|
| 163 |
create_sliders_inputs()
|
data/inputs_aggregated.csv
CHANGED
|
@@ -1,84 +1,84 @@
|
|
| 1 |
-
Implementation,Resource,Type,Unit,count,quantile_25,mean,median,quantile_75,std,min,max
|
| 2 |
-
Afforestation,Unspecified land,Physical,Mha/MtCO2,36,0.024593,0.115652,0.094927,0.197075,0.112368,0.002,0.384615
|
| 3 |
-
Afforestation,Unspecified water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0,0.092,0.092
|
| 4 |
-
Agroforestry,Arable land,Physical,Mha/MtCO2,12,0.039953,0.450767,0.064101,0.2141,0.90114,0.0025,3.030303
|
| 5 |
-
Bio-char,Electrical energy,Energy,MWh/MtCO2,6,
|
| 6 |
-
Bio-char,Forest biomass,Physical,Mt/MtCO2,6,0.34,0.472833,0.37,0.55525,0.201624,0.33,0.82
|
| 7 |
-
Bio-char,Non-forest biomass,Physical,Mt/MtCO2,8,1.142,1.291,1.3395,1.4885,0.33202,0.818,1.667
|
| 8 |
-
Bio-char,Unspecified biomass,Physical,Mt/MtCO2,48,0.943396,1.924255,1.209221,2.522177,1.937824,0.5,12.86
|
| 9 |
-
Bio-char,Unspecified land,Physical,Mha/MtCO2,4,0.056402,0.163201,0.163201,0.27,0.123321,0.056402,0.27
|
| 10 |
-
Bio-char,Unspecified water,Physical,bcm/MtCO2,9,0,0.000633,0.0001,0.0005,0.001377,-0.0005,0.003
|
| 11 |
-
BioCCS with anaerobic digestion/fermentation,Arable land,Physical,Mha/MtCO2,16,0.037937,0.100867,0.058844,0.103034,0.113758,0.022803,0.454545
|
| 12 |
-
BioCCS with anaerobic digestion/fermentation,Non-forest biomass,Physical,Mt/MtCO2,6,1.428571,2.689964,1.561743,2.128274,2.62425,1.315,8
|
| 13 |
-
BioCCS with anaerobic digestion/fermentation,Unspecified biomass,Physical,Mt/MtCO2,6,1.03357,1.379365,1.14,1.785,0.484913,0.998094,2
|
| 14 |
-
BioCCS with anaerobic digestion/fermentation,Unspecified water,Physical,bcm/MtCO2,14,0.395455,0.569805,0.565909,0.654546,0.183277,0.368182,0.913636
|
| 15 |
-
BioCCS with combustion,Arable land,Physical,Mha/MtCO2,32,0.038,0.20054,0.0584,0.094595,0.46566,0.022803,1.9451
|
| 16 |
-
BioCCS with combustion,Electrical energy,Energy,MWh/MtCO2,18,27777.78,130975.3,101000,111111.1,162410.3,0,500000
|
| 17 |
-
BioCCS with combustion,Forest biomass,Physical,Mt/MtCO2,8,0.560764,0.675481,0.59375,0.838675,0.144419,0.555556,0.8547
|
| 18 |
-
BioCCS with combustion,Thermal energy unspecified grade,Energy,MWh/MtCO2,22,347222.2,1006242,930611.1,1025000,1052440,0,4000000
|
| 19 |
-
BioCCS with combustion,Unspecified biomass,Physical,Mt/MtCO2,8,0.916667,1.264167,1.14,1.625,0.528225,0.666667,2
|
| 20 |
-
BioCCS with combustion,Unspecified chemical,Physical,Mt/MtCO2,20,0.00027,4.044119,0.00316,0.016,8.287794,0.00013,20.2
|
| 21 |
-
BioCCS with combustion,Unspecified water,Physical,bcm/MtCO2,32,0.006475,0.312937,0.201625,0.542046,0.343155,0.00063,1.296342
|
| 22 |
-
"Coastal revegetation (mangrove, seaweeds, etc.)",Non-arable land,Physical,Mha/MtCO2,11,0.043295,0.132018,0.120566,0.197449,0.117064,0.0078,0.328289
|
| 23 |
-
Cropland management,Arable land,Physical,Mha/MtCO2,14,0.27062,1.277946,0.420635,1.052335,2.329779,0.082645,9
|
| 24 |
-
Durable biobased products,Electrical energy,Energy,MWh/MtCO2,2,4000000,4000000,4000000,4000000,0,4000000,4000000
|
| 25 |
-
Durable biobased products,Non-arable land,Physical,Mha/MtCO2,2,6,6,6,6,0,6,6
|
| 26 |
-
Durable biobased products,Unspecified biomass,Physical,Mt/MtCO2,6,1.267857,2.607143,3.571429,3.892857,1.842663,0,4
|
| 27 |
-
Durable biobased products,Unspecified water,Physical,bcm/MtCO2,2,0.0002,0.0003,0.0003,0.0004,0.000283,0.0001,0.0005
|
| 28 |
-
Electrochemical DACCS,Electrical energy,Energy,MWh/MtCO2,10,333333.3,2032081,784125,2750000,2378833,252556,6222222
|
| 29 |
-
Electrochemical DACCS,Non-arable land,Physical,Mha/MtCO2,14,0.000272,0.002662,0.000324,0.000881,0.005657,0.000223,0.016
|
| 30 |
-
Electrochemical DACCS,Unspecified water,Physical,bcm/MtCO2,6,0.002,0.011,0.005,0.0215,0.012522,0.001,0.027
|
| 31 |
-
Electrochemical OAE,Electrical energy,Energy,MWh/MtCO2,8,1600000,2241250,2150000,2500000,1283594,830000,5000000
|
| 32 |
-
Electrochemical OAE,Non-arable land,Physical,Mha/MtCO2,2,0.00086,0.00101,0.00101,0.00116,0.000424,0.00071,0.00131
|
| 33 |
-
Electrochemical OAE,Shoreline,Physical,km/MtCO2,2,
|
| 34 |
-
Electrochemical OAE,Unspecified mineral,Physical,Mt/MtCO2,4,2.4,2.95,2.95,3.5,0.635085,2.4,3.5
|
| 35 |
-
Enhanced weathering,Arable land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,
|
| 36 |
-
Enhanced weathering,Basalt mineral,Physical,Mt/MtCO2,25,3.33,5.773042,4.3,5.3,4.967901,1,19
|
| 37 |
-
Enhanced weathering,Electrical energy,Energy,MWh/MtCO2,8,92850,593431.3,388750,500000,904565.7,9250,2775000
|
| 38 |
-
Enhanced weathering,Non-arable land,Physical,Mha/MtCO2,12,0.004429,11.43247,0.009065,28.57143,17.2264,0.0009,40
|
| 39 |
-
Enhanced weathering,Olivine mineral,Physical,Mt/MtCO2,26,0.909091,1.429511,1.25,1.58,0.941247,0.27027,3.7
|
| 40 |
-
Enhanced weathering,Unspecified mineral,Physical,Mt/MtCO2,19,1.315789,6.356736,2.5,8.625,7.726415,0.5,25
|
| 41 |
-
Enhanced weathering,Unspecified water,Physical,bcm/MtCO2,5,0.0005,0.010466,0.000866,0.000866,0.022102,0.0001,0.05
|
| 42 |
-
Enhanced weathering,Wollastonite mineral,Physical,Mt/MtCO2,8,1.321429,1.629643,1.564286,1.83,0.742366,0.56,3
|
| 43 |
-
Forest management,Unspecified land,Physical,Mha/MtCO2,2,0.438675,0.76865,0.76865,1.098625,0.93331,0.1087,1.4286
|
| 44 |
-
High-temp DACCS (solid-sorbent/liquid-solvent),Electrical energy,Energy,MWh/MtCO2,32,361111.1,423787.8,462961.1,500000,163178.3,77000,770000
|
| 45 |
-
High-temp DACCS (solid-sorbent/liquid-solvent),Non-arable land,Physical,Mha/MtCO2,23,2.
|
| 46 |
-
High-temp DACCS (solid-sorbent/liquid-solvent),Thermal energy high grade,Energy,MWh/MtCO2,38,1472222,1954835,1750000,2314815,643937.6,1125000,3800000
|
| 47 |
-
High-temp DACCS (solid-sorbent/liquid-solvent),Unspecified water,Physical,bcm/MtCO2,16,0.002502,0.010221,0.0047,0.00818,0.015887,
|
| 48 |
-
Low-temp DACCS (solid-sorbent/liquid-solvent),Electrical energy,Energy,MWh/MtCO2,44,218000,895320.2,462961.1,690000,1255695,130000,4611111
|
| 49 |
-
Low-temp DACCS (solid-sorbent/liquid-solvent),Non-arable land,Physical,Mha/MtCO2,23,6.
|
| 50 |
-
Low-temp DACCS (solid-sorbent/liquid-solvent),Thermal energy low grade,Energy,MWh/MtCO2,30,1432500,1678215,1500000,2062500,516794.3,638888.9,2722222
|
| 51 |
-
Low-temp DACCS (solid-sorbent/liquid-solvent),Unspecified water,Physical,bcm/MtCO2,14,7.
|
| 52 |
-
Mineral OAE / Ocean liming,Electrical energy,Energy,MWh/MtCO2,12,323625,1153583,606500,1755000,1286024,27000,4000000
|
| 53 |
-
Mineral OAE / Ocean liming,Non-arable land,Physical,Mha/MtCO2,4,0,0.000505,0.000355,0.00086,0.000632,0,0.00131
|
| 54 |
-
Mineral OAE / Ocean liming,Thermal energy unspecified grade,Energy,MWh/MtCO2,6,1388889,1257630,1458944,1529000,542588.8,160000,1550000
|
| 55 |
-
Mineral OAE / Ocean liming,Unspecified mineral,Physical,Mt/MtCO2,8,1.62,2.489,1.743,2.5895,1.554598,1.4,5
|
| 56 |
-
Mineral OAE / Ocean liming,Unspecified water,Physical,bcm/MtCO2,2,0.000678,0.000678,0.000678,0.000678,0,0.000678,0.000678
|
| 57 |
-
Mineral looping DACCS,Electrical energy,Energy,MWh/MtCO2,8,83333.33,1104167,83333.33,1375000,1793811,0,4000000
|
| 58 |
-
Mineral looping DACCS,Non-arable land,Physical,Mha/MtCO2,12,0.000587,0.001065,0.000639,0.000873,0.001064,0.000223,0.0033
|
| 59 |
-
Mineral looping DACCS,Thermal energy unspecified grade,Energy,MWh/MtCO2,4,1479167,1715278,1819444,2055556,534015.3,1000000,2222222
|
| 60 |
-
Mineral looping DACCS,Unspecified mineral,Physical,Mt/MtCO2,4,0.15,0.225,0.2,0.275,0.206155,0,0.5
|
| 61 |
-
Mineral looping DACCS,Unspecified water,Physical,bcm/MtCO2,6,0.000125,0.006762,0.0002,0.010068,0.011116,0,0.026714
|
| 62 |
-
Mineralization ex-situ,Electrical energy,Energy,MWh/MtCO2,8,500000,1062500,750000,1250000,979704.8,0,3000000
|
| 63 |
-
Mineralization ex-situ,Non-arable land,Physical,Mha/MtCO2,4,0.000298,0.000522,0.000436,0.000659,0.000342,0.000223,0.000991
|
| 64 |
-
Mineralization ex-situ,Olivine mineral,Physical,Mt/MtCO2,2,0.8,0.8,0.8,0.8,0,0.8,0.8
|
| 65 |
-
Mineralization ex-situ,Thermal energy unspecified grade,Energy,MWh/MtCO2,4,400000,575000,450000,625000,287228.1,400000,1000000
|
| 66 |
-
Mineralization ex-situ,Unspecified mineral,Physical,Mt/MtCO2,22,0.4375,3.935,3,7,3.647722,0.04,10
|
| 67 |
-
Mineralization ex-situ,Unspecified water,Physical,bcm/MtCO2,14,0.0002,0.050004,0.00545,0.0145,0.106908,0,0.3
|
| 68 |
-
Moisture swing DACCS,Electrical energy,Energy,MWh/MtCO2,4,316000,324666.7,324666.7,333333.3,10007.4,316000,333333.3
|
| 69 |
-
Moisture swing DACCS,Non-arable land,Physical,Mha/MtCO2,8,0.000169,0.000331,0.000272,0.00038,0.000319,
|
| 70 |
-
Moisture swing DACCS,Unspecified water,Physical,bcm/MtCO2,4,-0.00015,0.000427,0.000427,0.001,0.000662,-0.00015,0.001
|
| 71 |
-
Pasture management,Arable land,Physical,Mha/MtCO2,24,0.102253,0.442368,0.252422,0.522974,0.522145,0.071894,2.289744
|
| 72 |
-
Reforestation,Unspecified land,Physical,Mha/MtCO2,34,0.044459,1.564102,0.094927,0.1887,6.825803,0.002,38.8
|
| 73 |
-
Reforestation,Unspecified water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0,0.092,0.092
|
| 74 |
-
pH swing,Electrical energy,Energy,MWh/MtCO2,28,980000,2205112,1768000,3302500,1381909,660000,4670000
|
| 75 |
-
pH swing,Salt water,Physical,bcm/MtCO2,4,0.435,4.78,4.78,9.125,5.017174,0.435,9.125
|
| 76 |
-
pH swing,Unspecified water,Physical,bcm/MtCO2,4,0.217391,6.608696,6.608696,13,7.380043,0.217391,13
|
| 77 |
-
BioCCS with combustion,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 78 |
-
BioCCS with anaerobic digestion/fermentation,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 79 |
-
Electrochemical DACCS,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 80 |
-
Low-temp DACCS (solid-sorbent/liquid-solvent),Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 81 |
-
High-temp DACCS (solid-sorbent/liquid-solvent),Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 82 |
-
Mineral looping DACCS,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 83 |
-
Moisture swing DACCS,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 84 |
-
pH swing,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
|
|
|
| 1 |
+
Implementation,Method group,Resource,Resource group,Type,Unit,count,quantile_25,mean,median,quantile_75,std,min,max,Is secondary
|
| 2 |
+
Afforestation,Forestry,Unspecified land,Land,Physical,Mha/MtCO2,36,0.024593,0.115652,0.094927,0.197075,0.112368,0.002,0.384615,False
|
| 3 |
+
Afforestation,Forestry,Unspecified water,Water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0.0,0.092,0.092,True
|
| 4 |
+
Agroforestry,Forestry,Arable land,Land,Physical,Mha/MtCO2,12,0.039953,0.450767,0.064101,0.2141,0.90114,0.0025,3.030303,False
|
| 5 |
+
Bio-char,Bio-char,Electrical energy,Energy,Energy,MWh/MtCO2,6,87.5,167000.0,350.0,375000.0,258000.0,0.0,500000.0,True
|
| 6 |
+
Bio-char,Bio-char,Forest biomass,Biomass,Physical,Mt/MtCO2,6,0.34,0.472833,0.37,0.55525,0.201624,0.33,0.82,False
|
| 7 |
+
Bio-char,Bio-char,Non-forest biomass,Biomass,Physical,Mt/MtCO2,8,1.142,1.291,1.3395,1.4885,0.33202,0.818,1.667,False
|
| 8 |
+
Bio-char,Bio-char,Unspecified biomass,Biomass,Physical,Mt/MtCO2,48,0.943396,1.924255,1.209221,2.522177,1.937824,0.5,12.86,False
|
| 9 |
+
Bio-char,Bio-char,Unspecified land,Land,Physical,Mha/MtCO2,4,0.056402,0.163201,0.163201,0.27,0.123321,0.056402,0.27,True
|
| 10 |
+
Bio-char,Bio-char,Unspecified water,Water,Physical,bcm/MtCO2,9,0.0,0.000633,0.0001,0.0005,0.001377,-0.0005,0.003,True
|
| 11 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Arable land,Land,Physical,Mha/MtCO2,16,0.037937,0.100867,0.058844,0.103034,0.113758,0.022803,0.454545,False
|
| 12 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Non-forest biomass,Biomass,Physical,Mt/MtCO2,6,1.428571,2.689964,1.561743,2.128274,2.62425,1.315,8.0,False
|
| 13 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Unspecified biomass,Biomass,Physical,Mt/MtCO2,6,1.03357,1.379365,1.14,1.785,0.484913,0.998094,2.0,False
|
| 14 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Unspecified water,Water,Physical,bcm/MtCO2,14,0.395455,0.569805,0.565909,0.654546,0.183277,0.368182,0.913636,True
|
| 15 |
+
BioCCS with combustion,Bio-CCS,Arable land,Land,Physical,Mha/MtCO2,32,0.038,0.20054,0.0584,0.094595,0.46566,0.022803,1.9451,False
|
| 16 |
+
BioCCS with combustion,Bio-CCS,Electrical energy,Energy,Energy,MWh/MtCO2,18,27777.78,130975.3,101000.0,111111.1,162410.3,0.0,500000.0,True
|
| 17 |
+
BioCCS with combustion,Bio-CCS,Forest biomass,Biomass,Physical,Mt/MtCO2,8,0.560764,0.675481,0.59375,0.838675,0.144419,0.555556,0.8547,False
|
| 18 |
+
BioCCS with combustion,Bio-CCS,Thermal energy unspecified grade,Energy,Energy,MWh/MtCO2,22,347222.2,1006242.0,930611.1,1025000.0,1052440.0,0.0,4000000.0,True
|
| 19 |
+
BioCCS with combustion,Bio-CCS,Unspecified biomass,Biomass,Physical,Mt/MtCO2,8,0.916667,1.264167,1.14,1.625,0.528225,0.666667,2.0,False
|
| 20 |
+
BioCCS with combustion,Bio-CCS,Unspecified chemical,Chemicals,Physical,Mt/MtCO2,20,0.00027,4.044119,0.00316,0.016,8.287794,0.00013,20.2,True
|
| 21 |
+
BioCCS with combustion,Bio-CCS,Unspecified water,Water,Physical,bcm/MtCO2,32,0.006475,0.312937,0.201625,0.542046,0.343155,0.00063,1.296342,True
|
| 22 |
+
"Coastal revegetation (mangrove, seaweeds, etc.)",Blue carbon,Non-arable land,Land,Physical,Mha/MtCO2,11,0.043295,0.132018,0.120566,0.197449,0.117064,0.0078,0.328289,False
|
| 23 |
+
Cropland management,"Soil carbon sequestration (crop, grasslands)",Arable land,Land,Physical,Mha/MtCO2,14,0.27062,1.277946,0.420635,1.052335,2.329779,0.082645,9.0,False
|
| 24 |
+
Durable biobased products,Durable biobased products,Electrical energy,Energy,Energy,MWh/MtCO2,2,4000000.0,4000000.0,4000000.0,4000000.0,0.0,4000000.0,4000000.0,True
|
| 25 |
+
Durable biobased products,Durable biobased products,Non-arable land,Land,Physical,Mha/MtCO2,2,6.0,6.0,6.0,6.0,0.0,6.0,6.0,True
|
| 26 |
+
Durable biobased products,Durable biobased products,Unspecified biomass,Biomass,Physical,Mt/MtCO2,6,1.267857,2.607143,3.571429,3.892857,1.842663,0.0,4.0,False
|
| 27 |
+
Durable biobased products,Durable biobased products,Unspecified water,Water,Physical,bcm/MtCO2,2,0.0002,0.0003,0.0003,0.0004,0.000283,0.0001,0.0005,True
|
| 28 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,10,333333.3,2032081.0,784125.0,2750000.0,2378833.0,252556.0,6222222.0,False
|
| 29 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,14,0.000272,0.002662,0.000324,0.000881,0.005657,0.000223,0.016,True
|
| 30 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Unspecified water,Water,Physical,bcm/MtCO2,6,0.002,0.011,0.005,0.0215,0.012522,0.001,0.027,False
|
| 31 |
+
Electrochemical OAE,Ocean alkalinity enhancement,Electrical energy,Energy,Energy,MWh/MtCO2,8,1600000.0,2241250.0,2150000.0,2500000.0,1283594.0,830000.0,5000000.0,False
|
| 32 |
+
Electrochemical OAE,Ocean alkalinity enhancement,Non-arable land,Land,Physical,Mha/MtCO2,2,0.00086,0.00101,0.00101,0.00116,0.000424,0.00071,0.00131,True
|
| 33 |
+
Electrochemical OAE,Ocean alkalinity enhancement,Shoreline,Shoreline,Physical,km/MtCO2,2,0.012,0.012,0.012,0.012,0.0,0.012,0.012,False
|
| 34 |
+
Electrochemical OAE,Ocean alkalinity enhancement,Unspecified mineral,Minerals,Physical,Mt/MtCO2,4,2.4,2.95,2.95,3.5,0.635085,2.4,3.5,False
|
| 35 |
+
Enhanced weathering,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1e-06,2.5,True
|
| 36 |
+
Enhanced weathering,Enhanced weathering,Basalt mineral,Minerals,Physical,Mt/MtCO2,25,3.33,5.773042,4.3,5.3,4.967901,1.0,19.0,False
|
| 37 |
+
Enhanced weathering,Enhanced weathering,Electrical energy,Energy,Energy,MWh/MtCO2,8,92850.0,593431.3,388750.0,500000.0,904565.7,9250.0,2775000.0,True
|
| 38 |
+
Enhanced weathering,Enhanced weathering,Non-arable land,Land,Physical,Mha/MtCO2,12,0.004429,11.43247,0.009065,28.57143,17.2264,0.0009,40.0,True
|
| 39 |
+
Enhanced weathering,Enhanced weathering,Olivine mineral,Minerals,Physical,Mt/MtCO2,26,0.909091,1.429511,1.25,1.58,0.941247,0.27027,3.7,False
|
| 40 |
+
Enhanced weathering,Enhanced weathering,Unspecified mineral,Minerals,Physical,Mt/MtCO2,19,1.315789,6.356736,2.5,8.625,7.726415,0.5,25.0,False
|
| 41 |
+
Enhanced weathering,Enhanced weathering,Unspecified water,Water,Physical,bcm/MtCO2,5,0.0005,0.010466,0.000866,0.000866,0.022102,0.0001,0.05,True
|
| 42 |
+
Enhanced weathering,Enhanced weathering,Wollastonite mineral,Minerals,Physical,Mt/MtCO2,8,1.321429,1.629643,1.564286,1.83,0.742366,0.56,3.0,False
|
| 43 |
+
Forest management,Forestry,Unspecified land,Land,Physical,Mha/MtCO2,2,0.438675,0.76865,0.76865,1.098625,0.93331,0.1087,1.4286,False
|
| 44 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,32,361111.1,423787.8,462961.1,500000.0,163178.3,77000.0,770000.0,False
|
| 45 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,23,2.2e-08,0.000229,0.000223,0.000298,0.000299,1.5e-10,0.000991,False
|
| 46 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Thermal energy high grade,Energy,Energy,MWh/MtCO2,38,1472222.0,1954835.0,1750000.0,2314815.0,643937.6,1125000.0,3800000.0,False
|
| 47 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Unspecified water,Water,Physical,bcm/MtCO2,16,0.002502,0.010221,0.0047,0.00818,0.015887,5e-09,0.05,True
|
| 48 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,44,218000.0,895320.2,462961.1,690000.0,1255695.0,130000.0,4611111.0,False
|
| 49 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,23,6.25e-08,0.000238,0.000223,0.000298,0.000294,1e-11,0.000991,False
|
| 50 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Thermal energy low grade,Energy,Energy,MWh/MtCO2,30,1432500.0,1678215.0,1500000.0,2062500.0,516794.3,638888.9,2722222.0,False
|
| 51 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Unspecified water,Water,Physical,bcm/MtCO2,14,7.5e-10,0.003187,0.001008,0.004925,0.005238,-0.002,0.015,True
|
| 52 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Electrical energy,Energy,Energy,MWh/MtCO2,12,323625.0,1153583.0,606500.0,1755000.0,1286024.0,27000.0,4000000.0,True
|
| 53 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Non-arable land,Land,Physical,Mha/MtCO2,4,0.0,0.000505,0.000355,0.00086,0.000632,0.0,0.00131,True
|
| 54 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Thermal energy unspecified grade,Energy,Energy,MWh/MtCO2,6,1388889.0,1257630.0,1458944.0,1529000.0,542588.8,160000.0,1550000.0,True
|
| 55 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Unspecified mineral,Minerals,Physical,Mt/MtCO2,8,1.62,2.489,1.743,2.5895,1.554598,1.4,5.0,False
|
| 56 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Unspecified water,Water,Physical,bcm/MtCO2,2,0.000678,0.000678,0.000678,0.000678,0.0,0.000678,0.000678,True
|
| 57 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,8,83333.33,1104167.0,83333.33,1375000.0,1793811.0,0.0,4000000.0,False
|
| 58 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,12,0.000587,0.001065,0.000639,0.000873,0.001064,0.000223,0.0033,False
|
| 59 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Thermal energy unspecified grade,Energy,Energy,MWh/MtCO2,4,1479167.0,1715278.0,1819444.0,2055556.0,534015.3,1000000.0,2222222.0,False
|
| 60 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Unspecified mineral,Minerals,Physical,Mt/MtCO2,4,0.15,0.225,0.2,0.275,0.206155,0.0,0.5,False
|
| 61 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Unspecified water,Water,Physical,bcm/MtCO2,6,0.000125,0.006762,0.0002,0.010068,0.011116,0.0,0.026714,True
|
| 62 |
+
Mineralization ex-situ,Mineralization,Electrical energy,Energy,Energy,MWh/MtCO2,8,500000.0,1062500.0,750000.0,1250000.0,979704.8,0.0,3000000.0,False
|
| 63 |
+
Mineralization ex-situ,Mineralization,Non-arable land,Land,Physical,Mha/MtCO2,4,0.000298,0.000522,0.000436,0.000659,0.000342,0.000223,0.000991,False
|
| 64 |
+
Mineralization ex-situ,Mineralization,Olivine mineral,Minerals,Physical,Mt/MtCO2,2,0.8,0.8,0.8,0.8,0.0,0.8,0.8,False
|
| 65 |
+
Mineralization ex-situ,Mineralization,Thermal energy unspecified grade,Energy,Energy,MWh/MtCO2,4,400000.0,575000.0,450000.0,625000.0,287228.1,400000.0,1000000.0,False
|
| 66 |
+
Mineralization ex-situ,Mineralization,Unspecified mineral,Minerals,Physical,Mt/MtCO2,22,0.4375,3.935,3.0,7.0,3.647722,0.04,10.0,False
|
| 67 |
+
Mineralization ex-situ,Mineralization,Unspecified water,Water,Physical,bcm/MtCO2,14,0.0002,0.050004,0.00545,0.0145,0.106908,0.0,0.3,False
|
| 68 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,4,316000.0,324666.7,324666.7,333333.3,10007.4,316000.0,333333.3,False
|
| 69 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,8,0.000169,0.000331,0.000272,0.00038,0.000319,8e-06,0.000991,False
|
| 70 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Unspecified water,Water,Physical,bcm/MtCO2,4,-0.00015,0.000427,0.000427,0.001,0.000662,-0.00015,0.001,False
|
| 71 |
+
Pasture management,"Soil carbon sequestration (crop, grasslands)",Arable land,Land,Physical,Mha/MtCO2,24,0.102253,0.442368,0.252422,0.522974,0.522145,0.071894,2.289744,False
|
| 72 |
+
Reforestation,Forestry,Unspecified land,Land,Physical,Mha/MtCO2,34,0.044459,1.564102,0.094927,0.1887,6.825803,0.002,38.8,False
|
| 73 |
+
Reforestation,Forestry,Unspecified water,Water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0.0,0.092,0.092,True
|
| 74 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Electrical energy,Energy,Energy,MWh/MtCO2,28,980000.0,2205112.0,1768000.0,3302500.0,1381909.0,660000.0,4670000.0,False
|
| 75 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Salt water,Water,Physical,bcm/MtCO2,4,0.435,4.78,4.78,9.125,5.017174,0.435,9.125,False
|
| 76 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Unspecified water,Water,Physical,bcm/MtCO2,4,0.217391,6.608696,6.608696,13.0,7.380043,0.217391,13.0,False
|
| 77 |
+
BioCCS with combustion,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 78 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 79 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 80 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 81 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 82 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 83 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 84 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
data/inputs_template.xlsx
CHANGED
|
Binary files a/data/inputs_template.xlsx and b/data/inputs_template.xlsx differ
|
|
|
data/method_groups.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Forestry": [
|
| 3 |
+
"Afforestation",
|
| 4 |
+
"Agroforestry",
|
| 5 |
+
"Forest management",
|
| 6 |
+
"Reforestation"
|
| 7 |
+
],
|
| 8 |
+
"Bio-char": [
|
| 9 |
+
"Bio-char - Forest biomass",
|
| 10 |
+
"Bio-char - Non-forest biomass",
|
| 11 |
+
"Bio-char - Unspecified biomass"
|
| 12 |
+
],
|
| 13 |
+
"Bio-CCS": [
|
| 14 |
+
"BioCCS with anaerobic digestion/fermentation",
|
| 15 |
+
"BioCCS with anaerobic digestion/fermentation & growing biomass",
|
| 16 |
+
"BioCCS with combustion",
|
| 17 |
+
"BioCCS with combustion & growing biomass"
|
| 18 |
+
],
|
| 19 |
+
"Blue carbon": [
|
| 20 |
+
"Coastal revegetation (mangrove, seaweeds, etc.)"
|
| 21 |
+
],
|
| 22 |
+
"Soil carbon sequestration (crop, grasslands)": [
|
| 23 |
+
"Cropland management",
|
| 24 |
+
"Pasture management"
|
| 25 |
+
],
|
| 26 |
+
"Durable biobased products": [
|
| 27 |
+
"Durable biobased products"
|
| 28 |
+
],
|
| 29 |
+
"Direct air carbon capture and storage (DACCS)": [
|
| 30 |
+
"Electrochemical DACCS",
|
| 31 |
+
"High-temp DACCS (solid-sorbent/liquid-solvent)",
|
| 32 |
+
"Low-temp DACCS (solid-sorbent/liquid-solvent)",
|
| 33 |
+
"Mineral looping DACCS",
|
| 34 |
+
"Moisture swing DACCS"
|
| 35 |
+
],
|
| 36 |
+
"Ocean alkalinity enhancement": [
|
| 37 |
+
"Electrochemical OAE",
|
| 38 |
+
"Mineral OAE / Ocean liming"
|
| 39 |
+
],
|
| 40 |
+
"Enhanced weathering": [
|
| 41 |
+
"Enhanced weathering - Basalt mineral",
|
| 42 |
+
"Enhanced weathering - Olivine mineral",
|
| 43 |
+
"Enhanced weathering - Wollastonite mineral",
|
| 44 |
+
"Enhanced weathering - Unspecified mineral"
|
| 45 |
+
],
|
| 46 |
+
"Mineralization": [
|
| 47 |
+
"Mineralization ex-situ"
|
| 48 |
+
],
|
| 49 |
+
"Direct ocean carbon capture and storage (DOCCS)": [
|
| 50 |
+
"pH swing"
|
| 51 |
+
]
|
| 52 |
+
}
|
data/resource_template.xlsx
CHANGED
|
Binary files a/data/resource_template.xlsx and b/data/resource_template.xlsx differ
|
|
|
data/secondary_resources.csv
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
method,resource
|
| 2 |
-
Afforestation,Unspecified water
|
| 3 |
-
Reforestation,Unspecified water
|
| 4 |
-
Durable biobased products,Non-arable land
|
| 5 |
-
Durable biobased products,Electrical energy
|
| 6 |
-
Durable biobased products,Unspecified water
|
| 7 |
-
Bio-char - Forest biomass,Unspecified land
|
| 8 |
-
Bio-char - Forest biomass,Electrical energy
|
| 9 |
-
Bio-char - Forest biomass,Unspecified water
|
| 10 |
-
Bio-char - Non-forest biomass,Unspecified land
|
| 11 |
-
Bio-char - Non-forest biomass,Electrical energy
|
| 12 |
-
Bio-char - Non-forest biomass,Unspecified water
|
| 13 |
-
Bio-char - Unspecified biomass,Unspecified land
|
| 14 |
-
Bio-char - Unspecified biomass,Electrical energy
|
| 15 |
-
Bio-char - Unspecified biomass,Unspecified water
|
| 16 |
-
BioCCS with combustion,Unspecified chemical
|
| 17 |
-
BioCCS with combustion,Electrical energy
|
| 18 |
-
BioCCS with combustion,Thermal energy
|
| 19 |
-
BioCCS with combustion,Unspecified water
|
| 20 |
-
BioCCS with combustion & growing biomass,Unspecified chemical
|
| 21 |
-
BioCCS with combustion & growing biomass,Electrical energy
|
| 22 |
-
BioCCS with combustion & growing biomass,Thermal energy
|
| 23 |
-
BioCCS with combustion & growing biomass,Unspecified water
|
| 24 |
-
BioCCS with anaerobic digestion/fermentation,Unspecified water
|
| 25 |
-
BioCCS with anaerobic digestion/fermentation & growing biomass,Unspecified water
|
| 26 |
-
Enhanced weathering - Basalt mineral,Arable land
|
| 27 |
-
Enhanced weathering - Basalt mineral,Non-arable land
|
| 28 |
-
Enhanced weathering - Basalt mineral,Electrical energy
|
| 29 |
-
Enhanced weathering - Basalt mineral,Unspecified water
|
| 30 |
-
Enhanced weathering - Olivine mineral,Arable land
|
| 31 |
-
Enhanced weathering - Olivine mineral,Non-arable land
|
| 32 |
-
Enhanced weathering - Olivine mineral,Electrical energy
|
| 33 |
-
Enhanced weathering - Olivine mineral,Unspecified water
|
| 34 |
-
Enhanced weathering - Wollastonite mineral,Arable land
|
| 35 |
-
Enhanced weathering - Wollastonite mineral,Non-arable land
|
| 36 |
-
Enhanced weathering - Wollastonite mineral,Electrical energy
|
| 37 |
-
Enhanced weathering - Wollastonite mineral,Unspecified water
|
| 38 |
-
Enhanced weathering - Unspecified mineral,Arable land
|
| 39 |
-
Enhanced weathering - Unspecified mineral,Non-arable land
|
| 40 |
-
Enhanced weathering - Unspecified mineral,Electrical energy
|
| 41 |
-
Enhanced weathering - Unspecified mineral,Unspecified water
|
| 42 |
-
Mineral OAE / Ocean liming,Non-arable land
|
| 43 |
-
Mineral OAE / Ocean liming,Electrical energy
|
| 44 |
-
Mineral OAE / Ocean liming,Thermal energy
|
| 45 |
-
Mineral OAE / Ocean liming,Unspecified water
|
| 46 |
-
Electrochemical OAE,Non-arable land
|
| 47 |
-
Electrochemical DACCS,Non-arable land
|
| 48 |
-
Low-temp DACCS (solid-sorbent/liquid-solvent),Unspecified water
|
| 49 |
-
High-temp DACCS (solid-sorbent/liquid-solvent),Unspecified water
|
| 50 |
-
Mineral looping DACCS,Unspecified water
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/secondary_resources.json
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
[
|
| 3 |
+
"Afforestation",
|
| 4 |
+
"Unspecified water"
|
| 5 |
+
],
|
| 6 |
+
[
|
| 7 |
+
"Bio-char",
|
| 8 |
+
"Electrical energy"
|
| 9 |
+
],
|
| 10 |
+
[
|
| 11 |
+
"Bio-char",
|
| 12 |
+
"Unspecified land"
|
| 13 |
+
],
|
| 14 |
+
[
|
| 15 |
+
"Bio-char",
|
| 16 |
+
"Unspecified water"
|
| 17 |
+
],
|
| 18 |
+
[
|
| 19 |
+
"Bio-char - Forest biomass",
|
| 20 |
+
"Electrical energy"
|
| 21 |
+
],
|
| 22 |
+
[
|
| 23 |
+
"Bio-char - Forest biomass",
|
| 24 |
+
"Unspecified land"
|
| 25 |
+
],
|
| 26 |
+
[
|
| 27 |
+
"Bio-char - Forest biomass",
|
| 28 |
+
"Unspecified water"
|
| 29 |
+
],
|
| 30 |
+
[
|
| 31 |
+
"Bio-char - Non-forest biomass",
|
| 32 |
+
"Electrical energy"
|
| 33 |
+
],
|
| 34 |
+
[
|
| 35 |
+
"Bio-char - Non-forest biomass",
|
| 36 |
+
"Unspecified land"
|
| 37 |
+
],
|
| 38 |
+
[
|
| 39 |
+
"Bio-char - Non-forest biomass",
|
| 40 |
+
"Unspecified water"
|
| 41 |
+
],
|
| 42 |
+
[
|
| 43 |
+
"Bio-char - Unspecified biomass",
|
| 44 |
+
"Electrical energy"
|
| 45 |
+
],
|
| 46 |
+
[
|
| 47 |
+
"Bio-char - Unspecified biomass",
|
| 48 |
+
"Unspecified land"
|
| 49 |
+
],
|
| 50 |
+
[
|
| 51 |
+
"Bio-char - Unspecified biomass",
|
| 52 |
+
"Unspecified water"
|
| 53 |
+
],
|
| 54 |
+
[
|
| 55 |
+
"BioCCS with anaerobic digestion/fermentation",
|
| 56 |
+
"Unspecified water"
|
| 57 |
+
],
|
| 58 |
+
[
|
| 59 |
+
"BioCCS with anaerobic digestion/fermentation & growing biomass",
|
| 60 |
+
"Unspecified water"
|
| 61 |
+
],
|
| 62 |
+
[
|
| 63 |
+
"BioCCS with combustion",
|
| 64 |
+
"Electrical energy"
|
| 65 |
+
],
|
| 66 |
+
[
|
| 67 |
+
"BioCCS with combustion",
|
| 68 |
+
"Thermal energy unspecified grade"
|
| 69 |
+
],
|
| 70 |
+
[
|
| 71 |
+
"BioCCS with combustion",
|
| 72 |
+
"Unspecified chemical"
|
| 73 |
+
],
|
| 74 |
+
[
|
| 75 |
+
"BioCCS with combustion",
|
| 76 |
+
"Unspecified water"
|
| 77 |
+
],
|
| 78 |
+
[
|
| 79 |
+
"BioCCS with combustion & growing biomass",
|
| 80 |
+
"Electrical energy"
|
| 81 |
+
],
|
| 82 |
+
[
|
| 83 |
+
"BioCCS with combustion & growing biomass",
|
| 84 |
+
"Thermal energy unspecified grade"
|
| 85 |
+
],
|
| 86 |
+
[
|
| 87 |
+
"BioCCS with combustion & growing biomass",
|
| 88 |
+
"Unspecified chemical"
|
| 89 |
+
],
|
| 90 |
+
[
|
| 91 |
+
"BioCCS with combustion & growing biomass",
|
| 92 |
+
"Unspecified water"
|
| 93 |
+
],
|
| 94 |
+
[
|
| 95 |
+
"Durable biobased products",
|
| 96 |
+
"Electrical energy"
|
| 97 |
+
],
|
| 98 |
+
[
|
| 99 |
+
"Durable biobased products",
|
| 100 |
+
"Non-arable land"
|
| 101 |
+
],
|
| 102 |
+
[
|
| 103 |
+
"Durable biobased products",
|
| 104 |
+
"Unspecified water"
|
| 105 |
+
],
|
| 106 |
+
[
|
| 107 |
+
"Electrochemical DACCS",
|
| 108 |
+
"Non-arable land"
|
| 109 |
+
],
|
| 110 |
+
[
|
| 111 |
+
"Electrochemical OAE",
|
| 112 |
+
"Non-arable land"
|
| 113 |
+
],
|
| 114 |
+
[
|
| 115 |
+
"Enhanced weathering",
|
| 116 |
+
"Arable land"
|
| 117 |
+
],
|
| 118 |
+
[
|
| 119 |
+
"Enhanced weathering",
|
| 120 |
+
"Electrical energy"
|
| 121 |
+
],
|
| 122 |
+
[
|
| 123 |
+
"Enhanced weathering",
|
| 124 |
+
"Non-arable land"
|
| 125 |
+
],
|
| 126 |
+
[
|
| 127 |
+
"Enhanced weathering",
|
| 128 |
+
"Unspecified water"
|
| 129 |
+
],
|
| 130 |
+
[
|
| 131 |
+
"Enhanced weathering - Basalt mineral",
|
| 132 |
+
"Arable land"
|
| 133 |
+
],
|
| 134 |
+
[
|
| 135 |
+
"Enhanced weathering - Basalt mineral",
|
| 136 |
+
"Electrical energy"
|
| 137 |
+
],
|
| 138 |
+
[
|
| 139 |
+
"Enhanced weathering - Basalt mineral",
|
| 140 |
+
"Non-arable land"
|
| 141 |
+
],
|
| 142 |
+
[
|
| 143 |
+
"Enhanced weathering - Basalt mineral",
|
| 144 |
+
"Unspecified water"
|
| 145 |
+
],
|
| 146 |
+
[
|
| 147 |
+
"Enhanced weathering - Olivine mineral",
|
| 148 |
+
"Arable land"
|
| 149 |
+
],
|
| 150 |
+
[
|
| 151 |
+
"Enhanced weathering - Olivine mineral",
|
| 152 |
+
"Electrical energy"
|
| 153 |
+
],
|
| 154 |
+
[
|
| 155 |
+
"Enhanced weathering - Olivine mineral",
|
| 156 |
+
"Non-arable land"
|
| 157 |
+
],
|
| 158 |
+
[
|
| 159 |
+
"Enhanced weathering - Olivine mineral",
|
| 160 |
+
"Unspecified water"
|
| 161 |
+
],
|
| 162 |
+
[
|
| 163 |
+
"Enhanced weathering - Unspecified mineral",
|
| 164 |
+
"Arable land"
|
| 165 |
+
],
|
| 166 |
+
[
|
| 167 |
+
"Enhanced weathering - Unspecified mineral",
|
| 168 |
+
"Electrical energy"
|
| 169 |
+
],
|
| 170 |
+
[
|
| 171 |
+
"Enhanced weathering - Unspecified mineral",
|
| 172 |
+
"Non-arable land"
|
| 173 |
+
],
|
| 174 |
+
[
|
| 175 |
+
"Enhanced weathering - Unspecified mineral",
|
| 176 |
+
"Unspecified water"
|
| 177 |
+
],
|
| 178 |
+
[
|
| 179 |
+
"Enhanced weathering - Wollastonite mineral",
|
| 180 |
+
"Arable land"
|
| 181 |
+
],
|
| 182 |
+
[
|
| 183 |
+
"Enhanced weathering - Wollastonite mineral",
|
| 184 |
+
"Electrical energy"
|
| 185 |
+
],
|
| 186 |
+
[
|
| 187 |
+
"Enhanced weathering - Wollastonite mineral",
|
| 188 |
+
"Non-arable land"
|
| 189 |
+
],
|
| 190 |
+
[
|
| 191 |
+
"Enhanced weathering - Wollastonite mineral",
|
| 192 |
+
"Unspecified water"
|
| 193 |
+
],
|
| 194 |
+
[
|
| 195 |
+
"High-temp DACCS (solid-sorbent/liquid-solvent)",
|
| 196 |
+
"Unspecified water"
|
| 197 |
+
],
|
| 198 |
+
[
|
| 199 |
+
"Low-temp DACCS (solid-sorbent/liquid-solvent)",
|
| 200 |
+
"Unspecified water"
|
| 201 |
+
],
|
| 202 |
+
[
|
| 203 |
+
"Mineral OAE / Ocean liming",
|
| 204 |
+
"Electrical energy"
|
| 205 |
+
],
|
| 206 |
+
[
|
| 207 |
+
"Mineral OAE / Ocean liming",
|
| 208 |
+
"Non-arable land"
|
| 209 |
+
],
|
| 210 |
+
[
|
| 211 |
+
"Mineral OAE / Ocean liming",
|
| 212 |
+
"Thermal energy unspecified grade"
|
| 213 |
+
],
|
| 214 |
+
[
|
| 215 |
+
"Mineral OAE / Ocean liming",
|
| 216 |
+
"Unspecified water"
|
| 217 |
+
],
|
| 218 |
+
[
|
| 219 |
+
"Mineral looping DACCS",
|
| 220 |
+
"Unspecified water"
|
| 221 |
+
],
|
| 222 |
+
[
|
| 223 |
+
"Reforestation",
|
| 224 |
+
"Unspecified water"
|
| 225 |
+
]
|
| 226 |
+
]
|
data/tooltips.xlsx
ADDED
|
Binary file (8.68 kB). View file
|
|
|
data/update.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Run this script after any change to inputs_aggregated.csv to update all
|
| 3 |
+
generated data files and Excel templates.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python data/update.py
|
| 7 |
+
"""
|
| 8 |
+
import hashlib
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
ROOT = Path(__file__).parent.parent
|
| 14 |
+
PYTHON = sys.executable
|
| 15 |
+
|
| 16 |
+
scripts = [
|
| 17 |
+
("Coefficients + method groups + secondary resources", "data/generate_model_data.py"),
|
| 18 |
+
("Input template (inputs_template.xlsx)", "data/generate_input_template.py"),
|
| 19 |
+
("Resource template (resource_template.xlsx)", "data/generate_resource_template.py"),
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
print("=" * 60)
|
| 23 |
+
print("CRRA Optimization Tool — data update")
|
| 24 |
+
print("=" * 60)
|
| 25 |
+
|
| 26 |
+
for label, script in scripts:
|
| 27 |
+
print(f"\n▶ {label}")
|
| 28 |
+
result = subprocess.run(
|
| 29 |
+
[PYTHON, str(ROOT / script)],
|
| 30 |
+
cwd=str(ROOT),
|
| 31 |
+
capture_output=True,
|
| 32 |
+
text=True,
|
| 33 |
+
)
|
| 34 |
+
for line in result.stdout.splitlines():
|
| 35 |
+
print(f" {line}")
|
| 36 |
+
if result.returncode != 0:
|
| 37 |
+
print(f" ERROR:\n{result.stderr}")
|
| 38 |
+
sys.exit(result.returncode)
|
| 39 |
+
|
| 40 |
+
print("\n" + "=" * 60)
|
| 41 |
+
print("All files updated successfully.")
|
| 42 |
+
print("=" * 60)
|
| 43 |
+
|
| 44 |
+
# Update the hash file so the app knows the CSV is in sync
|
| 45 |
+
csv_path = ROOT / "data/inputs_aggregated.csv"
|
| 46 |
+
hash_path = ROOT / "data/.csv_hash"
|
| 47 |
+
hasher = hashlib.md5()
|
| 48 |
+
with open(csv_path, "rb") as f:
|
| 49 |
+
hasher.update(f.read())
|
| 50 |
+
with open(hash_path, "w") as f:
|
| 51 |
+
f.write(hasher.hexdigest())
|
data/update_cost_sliders_with_priority.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
|
| 3 |
-
import pandas as pd
|
| 4 |
-
|
| 5 |
-
# === Load JSON data ===
|
| 6 |
-
with open("cost_sliders.json", "r") as f:
|
| 7 |
-
cost_sliders = json.load(f)
|
| 8 |
-
|
| 9 |
-
# === Load resource priority CSV ===
|
| 10 |
-
df_priority = pd.read_csv("resource_priority.csv")
|
| 11 |
-
|
| 12 |
-
# === Validate columns ===
|
| 13 |
-
required_cols = {"method", "resource", "priority"}
|
| 14 |
-
if not required_cols.issubset(df_priority.columns):
|
| 15 |
-
raise ValueError(f"CSV must contain the columns: {required_cols}")
|
| 16 |
-
|
| 17 |
-
# === Update the JSON structure ===
|
| 18 |
-
for _, row in df_priority.iterrows():
|
| 19 |
-
method = row["method"]
|
| 20 |
-
resource = row["resource"]
|
| 21 |
-
priority = row["priority"]
|
| 22 |
-
|
| 23 |
-
if method in cost_sliders and resource in cost_sliders[method]:
|
| 24 |
-
cost_sliders[method][resource]["priority"] = priority
|
| 25 |
-
else:
|
| 26 |
-
print(f"⚠️ Warning: ({method} / {resource}) not found in JSON.")
|
| 27 |
-
|
| 28 |
-
# === Save the updated JSON ===
|
| 29 |
-
with open("cost_sliders_with_priority.json", "w") as f:
|
| 30 |
-
json.dump(cost_sliders, f, indent=2)
|
| 31 |
-
|
| 32 |
-
print("✅ Updated JSON saved to cost_sliders_with_priority.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
optimization/optimization.py
CHANGED
|
@@ -2,161 +2,225 @@ import numpy as np
|
|
| 2 |
from scipy.optimize import linprog
|
| 3 |
|
| 4 |
|
| 5 |
-
def
|
| 6 |
-
|
| 7 |
-
method_constraints,
|
| 8 |
-
method_costs,
|
| 9 |
-
custom_resources=None,
|
| 10 |
-
enabled_methods=None,
|
| 11 |
-
):
|
| 12 |
-
"""
|
| 13 |
-
Maximize total CO₂ removal subject to resource caps and method constraints.
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
resource_caps = dict(resource_caps)
|
| 21 |
-
method_costs = {m: dict(v) for m, v in method_costs.items()}
|
| 22 |
-
print("first enabled_methods ----------- ")
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
enabled_methods = enabled_methods or {}
|
| 26 |
-
|
| 27 |
-
for method, resources in method_costs.items():
|
| 28 |
for resource in list(resources):
|
| 29 |
if not enabled_methods.get(resource, {}).get(method, True):
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
for batch in (custom_resources or []):
|
| 35 |
-
|
| 36 |
"group": batch["group"],
|
| 37 |
"amount": float(batch["amount"]),
|
| 38 |
"primary_methods": set(batch.get("methods", {}).keys()),
|
| 39 |
}
|
| 40 |
-
custom_names = set(custom_batch_info)
|
| 41 |
-
ref_to_batches = {}
|
| 42 |
-
for bname, info in custom_batch_info.items():
|
| 43 |
-
ref_to_batches.setdefault(info["group"], []).append(bname)
|
| 44 |
-
if bname not in resource_caps:
|
| 45 |
-
resource_caps[bname] = info["amount"]
|
| 46 |
-
|
| 47 |
-
# ── Inject primary batch coefficients (fallback: use batch median if coef=0)
|
| 48 |
-
for batch in (custom_resources or []):
|
| 49 |
-
bname = batch["name"]
|
| 50 |
-
for method, coefs in batch["methods"].items():
|
| 51 |
-
if method_costs.get(method, {}).get(bname, 0.0) == 0.0:
|
| 52 |
-
method_costs.setdefault(method, {})[bname] = coefs["median"]
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
for method in batch["methods"]:
|
| 61 |
-
coef = method_costs.get(method, {}).get(r_key)
|
| 62 |
-
print(f" method_costs['{method}']['{r_key}'] = {coef}")
|
| 63 |
-
print(method_costs)
|
| 64 |
-
print(f"Total resources in matrix: {len(resource_caps)}")
|
| 65 |
-
print(f"Total resources in matrix: {resource_caps}")
|
| 66 |
-
print(f"Resources: {list(resource_caps.keys())}")
|
| 67 |
-
print(f"{'='*50}\n")
|
| 68 |
-
|
| 69 |
-
# ── Active methods ────────────────────────────────────────────────────────
|
| 70 |
-
methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
|
| 71 |
-
M = len(methods)
|
| 72 |
-
method_idx = {m: j for j, m in enumerate(methods)}
|
| 73 |
-
if not M:
|
| 74 |
-
return False, {"message": "No active methods."}
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
continue
|
| 83 |
-
for m in
|
| 84 |
if m not in primary_methods:
|
| 85 |
continue
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
z_pairs.append((m, bname))
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
|
| 94 |
-
print(f"z_pairs ({Z}): {z_pairs}")
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
A_rows, b_rows, constraint_labels = [], [], []
|
| 103 |
|
| 104 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
for r in resources_no_batches:
|
| 106 |
-
row = np.zeros(
|
| 107 |
-
for j, m in enumerate(
|
| 108 |
row[j] = method_costs.get(m, {}).get(r, 0.0)
|
| 109 |
-
if np.any(row[:
|
| 110 |
A_rows.append(row)
|
| 111 |
b_rows.append(resource_caps[r])
|
| 112 |
constraint_labels.append(("resource", r))
|
| 113 |
|
| 114 |
-
# (B) Standard resources
|
| 115 |
-
#
|
|
|
|
| 116 |
for r in resources_with_batches:
|
| 117 |
-
row = np.zeros(
|
| 118 |
-
for j, m in enumerate(
|
| 119 |
-
c_s_m = method_costs.get(m, {}).get(r, 0.0)
|
| 120 |
row[j] = c_s_m
|
| 121 |
-
for
|
| 122 |
-
if (m,
|
| 123 |
-
row[
|
| 124 |
if np.any(row != 0):
|
| 125 |
A_rows.append(row)
|
| 126 |
b_rows.append(resource_caps[r])
|
| 127 |
constraint_labels.append(("resource", r))
|
| 128 |
|
| 129 |
-
# (C) Custom batch capacity
|
| 130 |
-
|
| 131 |
-
|
|
|
|
| 132 |
continue
|
| 133 |
-
row = np.zeros(
|
| 134 |
-
for j, m in enumerate(
|
| 135 |
-
if (m,
|
| 136 |
continue
|
| 137 |
-
c_n_m = method_costs.get(m, {}).get(
|
| 138 |
if c_n_m > 0:
|
| 139 |
-
row[
|
| 140 |
if np.any(row != 0):
|
| 141 |
A_rows.append(row)
|
| 142 |
-
b_rows.append(resource_caps.get(
|
| 143 |
-
constraint_labels.append(("custom_batch",
|
| 144 |
|
| 145 |
-
# (
|
| 146 |
-
#
|
| 147 |
-
#
|
|
|
|
| 148 |
coupling_done = set()
|
| 149 |
-
for (m,
|
| 150 |
-
ref =
|
| 151 |
key = (m, ref)
|
| 152 |
if key in coupling_done:
|
| 153 |
continue
|
| 154 |
coupling_done.add(key)
|
| 155 |
-
j =
|
| 156 |
c_s_m = method_costs.get(m, {}).get(ref, 0.0)
|
| 157 |
-
batch_cols = [
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
| 160 |
row[j] = -1.0
|
| 161 |
for col in batch_cols:
|
| 162 |
row[col] = 1.0
|
|
@@ -165,7 +229,7 @@ def run_optimization(
|
|
| 165 |
constraint_labels.append(("coupling_D1", (m, ref)))
|
| 166 |
|
| 167 |
if c_s_m == 0:
|
| 168 |
-
row2 = np.zeros(
|
| 169 |
row2[j] = 1.0
|
| 170 |
for col in batch_cols:
|
| 171 |
row2[col] = -1.0
|
|
@@ -173,103 +237,245 @@ def run_optimization(
|
|
| 173 |
b_rows.append(0.0)
|
| 174 |
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 175 |
|
| 176 |
-
# (E) Method caps
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
constraint = method_constraints.get(m, {})
|
| 179 |
cap_type = constraint.get("cap_type", "percent")
|
| 180 |
cap_value = constraint.get("cap_value", 100)
|
| 181 |
if cap_type == "absolute":
|
| 182 |
-
row
|
| 183 |
-
row[j]
|
| 184 |
A_rows.append(row)
|
| 185 |
b_rows.append(cap_value)
|
| 186 |
constraint_labels.append(("method_cap", m))
|
| 187 |
else:
|
| 188 |
-
cap_ratio
|
| 189 |
-
row
|
| 190 |
-
row[:
|
| 191 |
-
row[j]
|
| 192 |
A_rows.append(row)
|
| 193 |
b_rows.append(0.0)
|
| 194 |
constraint_labels.append(("method_cap", m))
|
| 195 |
|
| 196 |
-
A_ub = np.array(A_rows) if A_rows else np.empty((0,
|
| 197 |
b_ub = np.array(b_rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
#
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
bounds
|
| 203 |
|
| 204 |
-
# ── Solve ─────────────────────────────────────────────────────────────────
|
| 205 |
try:
|
| 206 |
-
result = linprog(
|
| 207 |
if not result.success:
|
| 208 |
return False, {"message": result.message}
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
resource_usage
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
z_sum = sum(
|
| 230 |
-
sol[z_idx[(m, bn)]]
|
| 231 |
-
for bn in ref_to_batches.get(r, [])
|
| 232 |
-
if (m, bn) in z_idx
|
| 233 |
-
)
|
| 234 |
-
used = c_s_m * (y[j] - z_sum)
|
| 235 |
-
if used > 1e-9:
|
| 236 |
-
resource_usage[r][m] = float(used)
|
| 237 |
-
|
| 238 |
-
for bname, info in custom_batch_info.items():
|
| 239 |
-
primary_methods = info["primary_methods"]
|
| 240 |
-
for j, m in enumerate(methods):
|
| 241 |
-
if m not in primary_methods:
|
| 242 |
-
continue
|
| 243 |
-
c_n_m = method_costs.get(m, {}).get(bname, 0.0)
|
| 244 |
-
if c_n_m <= 0:
|
| 245 |
-
continue
|
| 246 |
-
used = c_n_m * sol[z_idx[(m, bname)]] if (m, bname) in z_idx else 0.0
|
| 247 |
-
if used > 1e-9:
|
| 248 |
-
resource_usage[bname][m] = float(used)
|
| 249 |
-
|
| 250 |
-
resource_used_total = {r: sum(v.values()) for r, v in resource_usage.items()}
|
| 251 |
-
|
| 252 |
-
all_caps = {**resource_caps,
|
| 253 |
-
**{bn: info["amount"] for bn, info in custom_batch_info.items()}}
|
| 254 |
-
resource_remaining = {r: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
|
| 255 |
-
for r in all_caps}
|
| 256 |
-
|
| 257 |
-
# ── Perturbation analysis: marginal CO₂ gain per +1 unit of each resource
|
| 258 |
-
resource_actual_gain = {}
|
| 259 |
-
for i, (ctype, cid) in enumerate(constraint_labels):
|
| 260 |
-
if ctype not in ("resource", "custom_batch"):
|
| 261 |
-
continue
|
| 262 |
-
b_perturbed = b_ub.copy()
|
| 263 |
-
b_perturbed[i] += 1.0
|
| 264 |
-
try:
|
| 265 |
-
perturbed = linprog(
|
| 266 |
-
c_obj, A_ub=A_ub, b_ub=b_perturbed, bounds=bounds, method="highs"
|
| 267 |
-
)
|
| 268 |
-
if perturbed.success:
|
| 269 |
-
new_total = float(perturbed.x[:M].sum())
|
| 270 |
-
resource_actual_gain[cid] = max(new_total - total_removed, 0.0)
|
| 271 |
-
except Exception:
|
| 272 |
-
pass
|
| 273 |
|
| 274 |
return True, {
|
| 275 |
"total_removed": total_removed,
|
|
@@ -281,4 +487,4 @@ def run_optimization(
|
|
| 281 |
}
|
| 282 |
|
| 283 |
except Exception as e:
|
| 284 |
-
return False, {"message": str(e)}
|
|
|
|
| 2 |
from scipy.optimize import linprog
|
| 3 |
|
| 4 |
|
| 5 |
+
def apply_enabled_methods(method_costs, enabled_methods):
|
| 6 |
+
"""Return a copy of method_costs with coefficients zeroed for disabled (method, resource) pairs.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
Args:
|
| 9 |
+
method_costs (dict): {method: {resource: coefficient}}.
|
| 10 |
+
enabled_methods (dict): {resource: {method: bool}} — False entries disable the coefficient.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
Returns:
|
| 13 |
+
dict: deep copy of method_costs with disabled coefficients set to 0.0.
|
| 14 |
+
"""
|
| 15 |
+
result = {m: dict(v) for m, v in method_costs.items()}
|
| 16 |
enabled_methods = enabled_methods or {}
|
| 17 |
+
for method, resources in result.items():
|
|
|
|
| 18 |
for resource in list(resources):
|
| 19 |
if not enabled_methods.get(resource, {}).get(method, True):
|
| 20 |
+
result[method][resource] = 0.0
|
| 21 |
+
return result
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def build_custom_batch_structures(method_costs, resource_caps, custom_resources):
|
| 25 |
+
"""Build the custom-batch index structures and inject fallback coefficients.
|
| 26 |
+
|
| 27 |
+
Mutates method_costs (adds fallback median coefficients) and resource_caps
|
| 28 |
+
(adds batch caps that are not yet present). Both dicts are already copies
|
| 29 |
+
when this function is called from run_optimization.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
method_costs (dict): {method: {resource: coefficient}} — mutated in place.
|
| 33 |
+
resource_caps (dict): {resource: cap} — mutated in place.
|
| 34 |
+
custom_resources (list | None): list of custom batch dicts with keys
|
| 35 |
+
name, group, amount, methods.
|
| 36 |
|
| 37 |
+
Returns:
|
| 38 |
+
tuple[dict, dict, set]:
|
| 39 |
+
custom_batch_specs – {batch_name: {group, amount, primary_methods}}.
|
| 40 |
+
group_to_custom_batches – {reference_resource: [batch_name, ...]}.
|
| 41 |
+
custom_batch_names – set of all batch names.
|
| 42 |
+
"""
|
| 43 |
+
custom_batch_specs = {}
|
| 44 |
for batch in (custom_resources or []):
|
| 45 |
+
custom_batch_specs[batch["name"]] = {
|
| 46 |
"group": batch["group"],
|
| 47 |
"amount": float(batch["amount"]),
|
| 48 |
"primary_methods": set(batch.get("methods", {}).keys()),
|
| 49 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
custom_batch_names = set(custom_batch_specs)
|
| 52 |
+
group_to_custom_batches = {}
|
| 53 |
+
for batch_name, specs in custom_batch_specs.items():
|
| 54 |
+
group_to_custom_batches.setdefault(specs["group"], []).append(batch_name)
|
| 55 |
+
if batch_name not in resource_caps:
|
| 56 |
+
resource_caps[batch_name] = specs["amount"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
# Fallback: use batch median coefficient if method has none
|
| 59 |
+
for batch in (custom_resources or []):
|
| 60 |
+
batch_name = batch["name"]
|
| 61 |
+
for method, coefs in batch["methods"].items():
|
| 62 |
+
if method_costs.get(method, {}).get(batch_name, 0.0) == 0.0:
|
| 63 |
+
method_costs.setdefault(method, {})[batch_name] = coefs["median"]
|
| 64 |
+
|
| 65 |
+
return custom_batch_specs, group_to_custom_batches, custom_batch_names
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_caps, method_costs):
|
| 69 |
+
"""Build the LP variable index for custom-batch draw variables z_{m,n}.
|
| 70 |
+
|
| 71 |
+
When the user adds a custom resource batch (e.g. "degraded land patch A"),
|
| 72 |
+
the solver needs to track separately how much of each CDR method's output
|
| 73 |
+
comes from that specific batch vs. the standard resource pool.
|
| 74 |
+
These "draw variables" z_{m,n} represent:
|
| 75 |
+
z_{m,n} = amount of CO₂ method m produces using custom batch n.
|
| 76 |
+
They sit alongside the main y_m variables in the LP column vector.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
active_methods (list[str]): ordered list of active CDR method names.
|
| 80 |
+
num_methods (int): len(active_methods) — index offset for z variables.
|
| 81 |
+
custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
|
| 82 |
+
resource_caps (dict): {resource: cap} — used to skip zero-cap batches.
|
| 83 |
+
method_costs (dict): {method: {resource: coefficient}}.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
tuple[list, dict, int]:
|
| 87 |
+
custom_usage_pairs – [(method, batch_name)] with non-zero coefficient.
|
| 88 |
+
custom_pair_to_index – {(method, batch_name): lp_column_index}.
|
| 89 |
+
num_lp_vars – total number of LP variables.
|
| 90 |
+
"""
|
| 91 |
+
custom_usage_pairs = []
|
| 92 |
+
for batch_name, specs in custom_batch_specs.items():
|
| 93 |
+
primary_methods = specs["primary_methods"]
|
| 94 |
+
if resource_caps.get(batch_name, 0.0) <= 0:
|
| 95 |
continue
|
| 96 |
+
for m in active_methods:
|
| 97 |
if m not in primary_methods:
|
| 98 |
continue
|
| 99 |
+
if method_costs.get(m, {}).get(batch_name, 0.0) > 0:
|
| 100 |
+
custom_usage_pairs.append((m, batch_name))
|
|
|
|
| 101 |
|
| 102 |
+
custom_pair_to_index = {pair: num_methods + i for i, pair in enumerate(custom_usage_pairs)}
|
| 103 |
+
num_lp_vars = num_methods + len(custom_usage_pairs)
|
| 104 |
+
return custom_usage_pairs, custom_pair_to_index, num_lp_vars
|
| 105 |
|
|
|
|
| 106 |
|
| 107 |
+
def build_constraint_matrix(
|
| 108 |
+
active_methods, method_to_index, num_lp_vars, num_methods,
|
| 109 |
+
resource_caps, method_costs, method_constraints,
|
| 110 |
+
custom_batch_specs, group_to_custom_batches,
|
| 111 |
+
resources_no_batches, resources_with_batches,
|
| 112 |
+
custom_usage_pairs, custom_pair_to_index,
|
| 113 |
+
):
|
| 114 |
+
"""Assemble the LP inequality constraint matrix (A_ub · x ≤ b_ub).
|
| 115 |
+
|
| 116 |
+
Builds five groups of constraints:
|
| 117 |
+
A – standard resources with no custom substitutes.
|
| 118 |
+
B – standard resources that have custom-batch substitutes.
|
| 119 |
+
C – custom batch capacity limits.
|
| 120 |
+
D1/D2 – coupling constraints between y_m and z_{m,n}.
|
| 121 |
+
E – per-method output caps (percent or absolute).
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
active_methods (list[str]): ordered active CDR method names.
|
| 125 |
+
method_to_index (dict): {method: lp_column_index}.
|
| 126 |
+
num_lp_vars (int): total LP variable count.
|
| 127 |
+
num_methods (int): number of y_m variables.
|
| 128 |
+
resource_caps (dict): {resource: cap}.
|
| 129 |
+
method_costs (dict): {method: {resource: coefficient}}.
|
| 130 |
+
method_constraints (dict): {method: {cap_type, cap_value}}.
|
| 131 |
+
custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
|
| 132 |
+
group_to_custom_batches (dict): {reference_resource: [batch_name]}.
|
| 133 |
+
resources_no_batches (list[str]): standard resources without substitutes.
|
| 134 |
+
resources_with_batches (list[str]): standard resources that have substitutes.
|
| 135 |
+
custom_usage_pairs (list): [(method, batch_name)].
|
| 136 |
+
custom_pair_to_index (dict): {(method, batch_name): lp_column_index}.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
tuple[np.ndarray, np.ndarray, list]:
|
| 140 |
+
A_ub – constraint coefficient matrix.
|
| 141 |
+
b_ub – constraint right-hand-side vector.
|
| 142 |
+
constraint_labels – list of (type_str, identifier) per row.
|
| 143 |
+
"""
|
| 144 |
A_rows, b_rows, constraint_labels = [], [], []
|
| 145 |
|
| 146 |
+
# ── CONSTRAINT GROUPS ─────────────────────────────────────────────────────
|
| 147 |
+
# Each group adds one row per resource/method to the matrix A_ub.
|
| 148 |
+
# The solver will find the y_m values that maximise total CO₂ removal while
|
| 149 |
+
# keeping every A_ub · x ≤ b_ub inequality satisfied.
|
| 150 |
+
#
|
| 151 |
+
# Plain-English summary of each group:
|
| 152 |
+
# A – "Don't use more of a standard resource than is available."
|
| 153 |
+
# B – "Same, but subtract the part already covered by custom batches."
|
| 154 |
+
# C – "Each custom batch can only supply up to its declared quantity."
|
| 155 |
+
# D – "Book-keeping: custom-batch draw variables can't exceed the method's
|
| 156 |
+
# total output, and if a method has no standard resource it must draw
|
| 157 |
+
# entirely from custom batches."
|
| 158 |
+
# E – "Respect each method's user-defined cap (% or absolute MtCO₂/yr)."
|
| 159 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 160 |
+
|
| 161 |
+
# (A) Standard resources — no custom substitutes.
|
| 162 |
+
# Rule: total resource consumed across all methods ≤ available amount.
|
| 163 |
for r in resources_no_batches:
|
| 164 |
+
row = np.zeros(num_lp_vars)
|
| 165 |
+
for j, m in enumerate(active_methods):
|
| 166 |
row[j] = method_costs.get(m, {}).get(r, 0.0)
|
| 167 |
+
if np.any(row[:num_methods] != 0):
|
| 168 |
A_rows.append(row)
|
| 169 |
b_rows.append(resource_caps[r])
|
| 170 |
constraint_labels.append(("resource", r))
|
| 171 |
|
| 172 |
+
# (B) Standard resources that have custom-batch substitutes.
|
| 173 |
+
# Rule: only the portion of the resource NOT already covered by custom batches
|
| 174 |
+
# counts against the standard pool cap. Formula: Σ cost(m,r)·(y_m − Σ z_{m,n}) ≤ cap(r)
|
| 175 |
for r in resources_with_batches:
|
| 176 |
+
row = np.zeros(num_lp_vars)
|
| 177 |
+
for j, m in enumerate(active_methods):
|
| 178 |
+
c_s_m = method_costs.get(m, {}).get(r, 0.0) # coefficient for standard resource r
|
| 179 |
row[j] = c_s_m
|
| 180 |
+
for batch_name in group_to_custom_batches.get(r, []):
|
| 181 |
+
if (m, batch_name) in custom_pair_to_index:
|
| 182 |
+
row[custom_pair_to_index[(m, batch_name)]] -= c_s_m # subtract batch draw
|
| 183 |
if np.any(row != 0):
|
| 184 |
A_rows.append(row)
|
| 185 |
b_rows.append(resource_caps[r])
|
| 186 |
constraint_labels.append(("resource", r))
|
| 187 |
|
| 188 |
+
# (C) Custom batch capacity.
|
| 189 |
+
# Rule: total resource drawn from this specific batch ≤ batch quantity Q_n.
|
| 190 |
+
for batch_name, specs in custom_batch_specs.items():
|
| 191 |
+
if resource_caps.get(batch_name, 0.0) <= 0:
|
| 192 |
continue
|
| 193 |
+
row = np.zeros(num_lp_vars)
|
| 194 |
+
for j, m in enumerate(active_methods):
|
| 195 |
+
if (m, batch_name) not in custom_pair_to_index:
|
| 196 |
continue
|
| 197 |
+
c_n_m = method_costs.get(m, {}).get(batch_name, 0.0) # coefficient for batch n
|
| 198 |
if c_n_m > 0:
|
| 199 |
+
row[custom_pair_to_index[(m, batch_name)]] = c_n_m
|
| 200 |
if np.any(row != 0):
|
| 201 |
A_rows.append(row)
|
| 202 |
+
b_rows.append(resource_caps.get(batch_name, 0.0))
|
| 203 |
+
constraint_labels.append(("custom_batch", batch_name))
|
| 204 |
|
| 205 |
+
# (D) Coupling constraints — link z_{m,n} draw variables to y_m output.
|
| 206 |
+
# D1: Σ z_{m,n} ≤ y_m — can't draw more from batches than the method produces.
|
| 207 |
+
# D2: y_m ≤ Σ z_{m,n} — only added when the method has no standard resource (c_s=0),
|
| 208 |
+
# which forces the method to source 100 % of its output from custom batches.
|
| 209 |
coupling_done = set()
|
| 210 |
+
for (m, batch_name) in custom_usage_pairs:
|
| 211 |
+
ref = custom_batch_specs[batch_name]["group"]
|
| 212 |
key = (m, ref)
|
| 213 |
if key in coupling_done:
|
| 214 |
continue
|
| 215 |
coupling_done.add(key)
|
| 216 |
+
j = method_to_index[m]
|
| 217 |
c_s_m = method_costs.get(m, {}).get(ref, 0.0)
|
| 218 |
+
batch_cols = [
|
| 219 |
+
custom_pair_to_index[(m, sibling)]
|
| 220 |
+
for sibling in group_to_custom_batches.get(ref, [])
|
| 221 |
+
if (m, sibling) in custom_pair_to_index
|
| 222 |
+
]
|
| 223 |
+
row = np.zeros(num_lp_vars)
|
| 224 |
row[j] = -1.0
|
| 225 |
for col in batch_cols:
|
| 226 |
row[col] = 1.0
|
|
|
|
| 229 |
constraint_labels.append(("coupling_D1", (m, ref)))
|
| 230 |
|
| 231 |
if c_s_m == 0:
|
| 232 |
+
row2 = np.zeros(num_lp_vars)
|
| 233 |
row2[j] = 1.0
|
| 234 |
for col in batch_cols:
|
| 235 |
row2[col] = -1.0
|
|
|
|
| 237 |
b_rows.append(0.0)
|
| 238 |
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 239 |
|
| 240 |
+
# (E) Method output caps — respect the user's per-method limits from Tab 2.
|
| 241 |
+
# Absolute cap: y_m ≤ cap_value (in MtCO₂/yr).
|
| 242 |
+
# Percent cap: y_m ≤ cap_ratio · Σ y_all (fraction of total portfolio output).
|
| 243 |
+
for j, m in enumerate(active_methods):
|
| 244 |
constraint = method_constraints.get(m, {})
|
| 245 |
cap_type = constraint.get("cap_type", "percent")
|
| 246 |
cap_value = constraint.get("cap_value", 100)
|
| 247 |
if cap_type == "absolute":
|
| 248 |
+
row = np.zeros(num_lp_vars)
|
| 249 |
+
row[j] = 1.0
|
| 250 |
A_rows.append(row)
|
| 251 |
b_rows.append(cap_value)
|
| 252 |
constraint_labels.append(("method_cap", m))
|
| 253 |
else:
|
| 254 |
+
cap_ratio = cap_value / 100.0
|
| 255 |
+
row = np.zeros(num_lp_vars)
|
| 256 |
+
row[:num_methods] -= cap_ratio
|
| 257 |
+
row[j] += 1.0
|
| 258 |
A_rows.append(row)
|
| 259 |
b_rows.append(0.0)
|
| 260 |
constraint_labels.append(("method_cap", m))
|
| 261 |
|
| 262 |
+
A_ub = np.array(A_rows) if A_rows else np.empty((0, num_lp_vars))
|
| 263 |
b_ub = np.array(b_rows)
|
| 264 |
+
return A_ub, b_ub, constraint_labels
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def extract_resource_usage(
|
| 268 |
+
active_methods, num_methods, method_outputs, lp_solution, method_costs,
|
| 269 |
+
resource_caps, custom_batch_specs, group_to_custom_batches,
|
| 270 |
+
custom_pair_to_index, standard_resources,
|
| 271 |
+
resources_no_batches, resources_with_batches, custom_batch_names,
|
| 272 |
+
):
|
| 273 |
+
"""Compute per-method resource consumption from the LP solution.
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
active_methods (list[str]): ordered active CDR method names.
|
| 277 |
+
num_methods (int): number of y_m variables.
|
| 278 |
+
method_outputs (np.ndarray): y_m values from LP solution (length num_methods).
|
| 279 |
+
lp_solution (np.ndarray): full LP solution vector x.
|
| 280 |
+
method_costs (dict): {method: {resource: coefficient}}.
|
| 281 |
+
resource_caps (dict): {resource: cap} — used for resource_remaining.
|
| 282 |
+
custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
|
| 283 |
+
group_to_custom_batches (dict): {reference_resource: [batch_name]}.
|
| 284 |
+
custom_pair_to_index (dict): {(method, batch_name): lp_column_index}.
|
| 285 |
+
standard_resources (list[str]): all non-batch resource names.
|
| 286 |
+
resources_no_batches (list[str]): standard resources without substitutes.
|
| 287 |
+
resources_with_batches (list[str]): standard resources with substitutes.
|
| 288 |
+
custom_batch_names (set[str]): set of all custom batch names.
|
| 289 |
+
|
| 290 |
+
Returns:
|
| 291 |
+
tuple[dict, dict, dict]:
|
| 292 |
+
resource_usage – {resource: {method: qty_used}}.
|
| 293 |
+
resource_used_total – {resource: total_qty_used}.
|
| 294 |
+
resource_remaining – {resource: qty_remaining}.
|
| 295 |
+
"""
|
| 296 |
+
all_resource_names = standard_resources + list(custom_batch_names)
|
| 297 |
+
resource_usage = {r: {} for r in all_resource_names}
|
| 298 |
+
|
| 299 |
+
# 1e-9 is a near-zero threshold: LP solvers return tiny floating-point residuals
|
| 300 |
+
# (e.g. 0.000000001) instead of exact 0. Values below this threshold are treated
|
| 301 |
+
# as zero so they don't appear as spurious resource usage in the output.
|
| 302 |
+
for r in resources_no_batches:
|
| 303 |
+
for j, m in enumerate(active_methods):
|
| 304 |
+
used = method_costs.get(m, {}).get(r, 0.0) * method_outputs[j]
|
| 305 |
+
if used > 1e-9:
|
| 306 |
+
resource_usage[r][m] = float(used)
|
| 307 |
+
|
| 308 |
+
for r in resources_with_batches:
|
| 309 |
+
for j, m in enumerate(active_methods):
|
| 310 |
+
c_s_m = method_costs.get(m, {}).get(r, 0.0)
|
| 311 |
+
z_sum = sum(
|
| 312 |
+
lp_solution[custom_pair_to_index[(m, batch_name)]]
|
| 313 |
+
for batch_name in group_to_custom_batches.get(r, [])
|
| 314 |
+
if (m, batch_name) in custom_pair_to_index
|
| 315 |
+
)
|
| 316 |
+
used = c_s_m * (method_outputs[j] - z_sum)
|
| 317 |
+
if used > 1e-9:
|
| 318 |
+
resource_usage[r][m] = float(used)
|
| 319 |
+
|
| 320 |
+
for batch_name, specs in custom_batch_specs.items():
|
| 321 |
+
primary_methods = specs["primary_methods"]
|
| 322 |
+
for j, m in enumerate(active_methods):
|
| 323 |
+
if m not in primary_methods:
|
| 324 |
+
continue
|
| 325 |
+
c_n_m = method_costs.get(m, {}).get(batch_name, 0.0)
|
| 326 |
+
if c_n_m <= 0:
|
| 327 |
+
continue
|
| 328 |
+
used = (
|
| 329 |
+
c_n_m * lp_solution[custom_pair_to_index[(m, batch_name)]]
|
| 330 |
+
if (m, batch_name) in custom_pair_to_index
|
| 331 |
+
else 0.0
|
| 332 |
+
)
|
| 333 |
+
if used > 1e-9:
|
| 334 |
+
resource_usage[batch_name][m] = float(used)
|
| 335 |
+
|
| 336 |
+
resource_used_total = {r: sum(v.values()) for r, v in resource_usage.items()}
|
| 337 |
+
all_caps = {
|
| 338 |
+
**resource_caps,
|
| 339 |
+
**{batch_name: specs["amount"] for batch_name, specs in custom_batch_specs.items()},
|
| 340 |
+
}
|
| 341 |
+
resource_remaining = {
|
| 342 |
+
r: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
|
| 343 |
+
for r in all_caps
|
| 344 |
+
}
|
| 345 |
+
return resource_usage, resource_used_total, resource_remaining
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def run_perturbation(constraint_labels, A_ub, b_ub, objective_coefficients, bounds, num_methods, total_removed):
|
| 349 |
+
"""Compute marginal CDR gain per unit for each binding resource constraint.
|
| 350 |
+
|
| 351 |
+
Re-solves the LP with each resource constraint right-hand side increased by 1
|
| 352 |
+
to measure the actual CO₂ gain that one extra unit would unlock.
|
| 353 |
+
|
| 354 |
+
Args:
|
| 355 |
+
constraint_labels (list): list of (type_str, identifier) per constraint row.
|
| 356 |
+
A_ub (np.ndarray): LP constraint coefficient matrix.
|
| 357 |
+
b_ub (np.ndarray): LP constraint right-hand-side vector.
|
| 358 |
+
objective_coefficients (np.ndarray): LP objective vector.
|
| 359 |
+
bounds (list): variable bounds for linprog.
|
| 360 |
+
num_methods (int): number of y_m variables (for summing total removed).
|
| 361 |
+
total_removed (float): baseline total CO₂ removed.
|
| 362 |
+
|
| 363 |
+
Returns:
|
| 364 |
+
dict: {resource_or_batch_name: CDR_gain_for_plus_1_unit (float)}.
|
| 365 |
+
"""
|
| 366 |
+
resource_actual_gain = {}
|
| 367 |
+
for i, (ctype, cid) in enumerate(constraint_labels):
|
| 368 |
+
if ctype not in ("resource", "custom_batch"):
|
| 369 |
+
continue
|
| 370 |
+
b_perturbed = b_ub.copy()
|
| 371 |
+
b_perturbed[i] += 1.0
|
| 372 |
+
try:
|
| 373 |
+
perturbed = linprog(
|
| 374 |
+
objective_coefficients, A_ub=A_ub, b_ub=b_perturbed, bounds=bounds, method="highs"
|
| 375 |
+
)
|
| 376 |
+
if perturbed.success:
|
| 377 |
+
new_total = float(perturbed.x[:num_methods].sum())
|
| 378 |
+
resource_actual_gain[cid] = max(new_total - total_removed, 0.0)
|
| 379 |
+
except Exception:
|
| 380 |
+
pass
|
| 381 |
+
return resource_actual_gain
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def run_optimization(
|
| 385 |
+
resource_caps,
|
| 386 |
+
method_constraints,
|
| 387 |
+
method_costs,
|
| 388 |
+
custom_resources=None,
|
| 389 |
+
enabled_methods=None,
|
| 390 |
+
):
|
| 391 |
+
"""Maximize total CO₂ removal subject to resource caps and method constraints.
|
| 392 |
+
|
| 393 |
+
Solves a linear programme (HiGHS solver via scipy.optimize.linprog).
|
| 394 |
+
Also runs a perturbation analysis: for each binding resource constraint,
|
| 395 |
+
re-solves with cap + 1 to compute the marginal CDR gain per unit.
|
| 396 |
+
|
| 397 |
+
LP variables (two types, packed into one vector x):
|
| 398 |
+
y_m = MtCO₂ removed by CDR method m — the main "answer" (indices 0..num_methods-1).
|
| 399 |
+
z_{m,n} = MtCO₂ that method m draws specifically from custom batch n — a helper
|
| 400 |
+
variable introduced when the user adds a custom resource batch; it lets the
|
| 401 |
+
solver track which batch supplies which method without double-counting
|
| 402 |
+
(indices num_methods..num_lp_vars-1).
|
| 403 |
+
|
| 404 |
+
Args:
|
| 405 |
+
resource_caps (dict): {resource: available amount (float)}.
|
| 406 |
+
method_constraints (dict): {method: {active (bool), cap_type (str), cap_value (float)}}.
|
| 407 |
+
method_costs (dict): {method: {resource: coefficient (float)}}.
|
| 408 |
+
custom_resources (list | None): list of custom batch dicts with keys
|
| 409 |
+
name, group, amount, methods.
|
| 410 |
+
enabled_methods (dict | None): {resource: {method: bool}} — False entries
|
| 411 |
+
zero-out the corresponding coefficient before solving.
|
| 412 |
+
|
| 413 |
+
Returns:
|
| 414 |
+
tuple[bool, dict]:
|
| 415 |
+
(True, result) on success, where result contains:
|
| 416 |
+
total_removed (float), method_usage (dict), resource_usage (dict),
|
| 417 |
+
resource_used_total (dict), resource_remaining (dict),
|
| 418 |
+
resource_actual_gain (dict).
|
| 419 |
+
(False, {"message": str}) on failure.
|
| 420 |
+
"""
|
| 421 |
+
resource_caps = dict(resource_caps)
|
| 422 |
+
method_costs = apply_enabled_methods(method_costs, enabled_methods)
|
| 423 |
+
|
| 424 |
+
custom_batch_specs, group_to_custom_batches, custom_batch_names = build_custom_batch_structures(
|
| 425 |
+
method_costs, resource_caps, custom_resources
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
active_methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
|
| 429 |
+
num_methods = len(active_methods)
|
| 430 |
+
method_to_index = {m: j for j, m in enumerate(active_methods)}
|
| 431 |
+
if not num_methods:
|
| 432 |
+
return False, {"message": "No active methods."}
|
| 433 |
+
|
| 434 |
+
custom_usage_pairs, custom_pair_to_index, num_lp_vars = build_lp_indices(
|
| 435 |
+
active_methods, num_methods, custom_batch_specs, resource_caps, method_costs
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
standard_resources = [r for r in resource_caps if r not in custom_batch_names]
|
| 439 |
+
resources_with_batches = [r for r in standard_resources if r in group_to_custom_batches]
|
| 440 |
+
resources_no_batches = [r for r in standard_resources if r not in group_to_custom_batches]
|
| 441 |
+
|
| 442 |
+
A_ub, b_ub, constraint_labels = build_constraint_matrix(
|
| 443 |
+
active_methods, method_to_index, num_lp_vars, num_methods,
|
| 444 |
+
resource_caps, method_costs, method_constraints,
|
| 445 |
+
custom_batch_specs, group_to_custom_batches,
|
| 446 |
+
resources_no_batches, resources_with_batches,
|
| 447 |
+
custom_usage_pairs, custom_pair_to_index,
|
| 448 |
+
)
|
| 449 |
|
| 450 |
+
# linprog minimises by convention, so we negate the objective to maximise Σ y_m.
|
| 451 |
+
objective_coefficients = np.zeros(num_lp_vars)
|
| 452 |
+
objective_coefficients[:num_methods] = -1.0 # only y_m variables enter the objective
|
| 453 |
+
bounds = [(0, None)] * num_lp_vars # all variables must be ≥ 0
|
| 454 |
|
|
|
|
| 455 |
try:
|
| 456 |
+
result = linprog(objective_coefficients, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method="highs")
|
| 457 |
if not result.success:
|
| 458 |
return False, {"message": result.message}
|
| 459 |
|
| 460 |
+
lp_solution = result.x
|
| 461 |
+
method_outputs = lp_solution[:num_methods]
|
| 462 |
+
total_removed = float(method_outputs.sum())
|
| 463 |
+
method_usage = {
|
| 464 |
+
active_methods[j]: float(method_outputs[j])
|
| 465 |
+
for j in range(num_methods) if method_outputs[j] > 1e-9
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
resource_usage, resource_used_total, resource_remaining = extract_resource_usage(
|
| 469 |
+
active_methods, num_methods, method_outputs, lp_solution, method_costs,
|
| 470 |
+
resource_caps, custom_batch_specs, group_to_custom_batches,
|
| 471 |
+
custom_pair_to_index, standard_resources,
|
| 472 |
+
resources_no_batches, resources_with_batches, custom_batch_names,
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
resource_actual_gain = run_perturbation(
|
| 476 |
+
constraint_labels, A_ub, b_ub, objective_coefficients, bounds,
|
| 477 |
+
num_methods, total_removed,
|
| 478 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
return True, {
|
| 481 |
"total_removed": total_removed,
|
|
|
|
| 487 |
}
|
| 488 |
|
| 489 |
except Exception as e:
|
| 490 |
+
return False, {"message": str(e)}
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
tabs/tab0_welcome.py
CHANGED
|
@@ -1,34 +1,43 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
from config.config import CO2_UNIT
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
<
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
<
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
<ol
|
| 23 |
-
|
| 24 |
-
</
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
</
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
""", unsafe_allow_html=True)
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from config.config import CO2_UNIT
|
| 3 |
+
from utils.ui_helpers import show_bar
|
| 4 |
+
|
| 5 |
+
def render_tab():
|
| 6 |
+
"""Render the Welcome tab with a step-by-step guide to using the tool.
|
| 7 |
+
|
| 8 |
+
Returns:
|
| 9 |
+
None
|
| 10 |
+
"""
|
| 11 |
+
st.markdown('<h1 class="crra-heading">Welcome!</h1>', unsafe_allow_html=True)
|
| 12 |
+
show_bar()
|
| 13 |
+
|
| 14 |
+
st.markdown(f"""
|
| 15 |
+
<div class='custom-corps'>
|
| 16 |
+
|
| 17 |
+
This tool helps you optimize your carbon direct removal portfolio by adjusting resource usage and constraints.
|
| 18 |
+
|
| 19 |
+
<h3 class="wt wt-1">Required:</h3>
|
| 20 |
+
<ol>
|
| 21 |
+
<li><b>Resource inputs</b>: provide the total available quantities of resources for 1 year.</li>
|
| 22 |
+
</ol>
|
| 23 |
+
|
| 24 |
+
<h3 class="wt wt-2">Optional:</h3>
|
| 25 |
+
<ol start="2">
|
| 26 |
+
<li><b>Method constraints</b>: select which carbon removal methods are active and set their <b>maximum deployment cap</b> (as % of total potential or absolute value in {CO2_UNIT}/yr).</li>
|
| 27 |
+
<li><b>Resource efficiency coefficients</b>: adjust the resource usage for each method.</li>
|
| 28 |
+
</ol>
|
| 29 |
+
|
| 30 |
+
<h3 class="wt wt-3">Outputs:</h3>
|
| 31 |
+
<ol start="4">
|
| 32 |
+
<li><b>Portfolio generation</b>: run the optimization to find the best portfolio: how much CO₂ is removed, what methods are used, and in what proportions.</li>
|
| 33 |
+
</ol>
|
| 34 |
+
|
| 35 |
+
<h3 class="wt wt-4">Explore:</h3>
|
| 36 |
+
<ol start="5">
|
| 37 |
+
<li><b>Sensitivity analysis</b>: analyze how changes in method caps or resource availability affect the portfolio.</li>
|
| 38 |
+
<li><b>Scenario comparison</b>: compare different scenarios based on the optimization results.</li>
|
| 39 |
+
</ol>
|
| 40 |
+
|
| 41 |
+
Let’s get started! Use the side tabs to navigate through the sections.
|
| 42 |
+
</div>
|
| 43 |
""", unsafe_allow_html=True)
|
tabs/tab1_inputs.py
CHANGED
|
@@ -1,818 +1,869 @@
|
|
| 1 |
-
import pandas as pd
|
| 2 |
-
import streamlit as st
|
| 3 |
-
|
| 4 |
-
from config.config import (resource_groups, resource_order, resource_tooltips,
|
| 5 |
-
resource_unit_map, method_groups, CO2_UNIT, secondary_resources)
|
| 6 |
-
from utils.data_utils import render_resource_input, strip_unit_suffix
|
| 7 |
-
from utils.ui_helpers import show_bar
|
| 8 |
-
from io import BytesIO
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
if
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
""
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
batch
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
for
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
)
|
| 817 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
st.error("No resources defined yet. Set availability above to populate the summary table.")
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import streamlit as st
|
| 3 |
+
|
| 4 |
+
from config.config import (resource_groups, resource_order, resource_tooltips,
|
| 5 |
+
resource_unit_map, method_groups, CO2_UNIT, secondary_resources)
|
| 6 |
+
from utils.data_utils import render_resource_input, strip_unit_suffix, get_resource_to_group, get_resource_to_unit, get_method_group
|
| 7 |
+
from utils.ui_helpers import show_bar
|
| 8 |
+
from io import BytesIO
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def standard_resource_names():
|
| 12 |
+
"""Return the set of all standard resource names defined in resource_groups config.
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
set[str]: flat set of every resource name across all resource groups.
|
| 16 |
+
"""
|
| 17 |
+
return {r for rlist in resource_groups.values() for r in rlist}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def init_custom_resources():
|
| 21 |
+
"""Initialise custom resource session state keys and coerce amounts/coefficients to float.
|
| 22 |
+
|
| 23 |
+
Ensures custom_resources, enabled_methods, and auto_disabled_methods exist in
|
| 24 |
+
session state, then sanitises every batch so numeric fields are float (not str).
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
None
|
| 28 |
+
"""
|
| 29 |
+
if "custom_resources" not in st.session_state:
|
| 30 |
+
st.session_state.custom_resources = []
|
| 31 |
+
if "enabled_methods" not in st.session_state:
|
| 32 |
+
st.session_state.enabled_methods = {}
|
| 33 |
+
if "auto_disabled_methods" not in st.session_state:
|
| 34 |
+
st.session_state.auto_disabled_methods = set()
|
| 35 |
+
for batch in st.session_state.custom_resources:
|
| 36 |
+
try:
|
| 37 |
+
batch["amount"] = float(batch["amount"])
|
| 38 |
+
except (ValueError, TypeError):
|
| 39 |
+
batch["amount"] = 0.0
|
| 40 |
+
for m, coefs in batch.get("methods", {}).items():
|
| 41 |
+
for key in ("min", "median", "max"):
|
| 42 |
+
try:
|
| 43 |
+
coefs[key] = float(coefs[key])
|
| 44 |
+
except (ValueError, TypeError):
|
| 45 |
+
coefs[key] = 0.0
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@st.dialog("Add a new resource", width="large")
|
| 49 |
+
def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, preset_resources: list | None = None):
|
| 50 |
+
"""Dialog for adding or editing a custom resource batch.
|
| 51 |
+
|
| 52 |
+
Opens a modal form where the user picks a name, reference resource group,
|
| 53 |
+
available quantity, and per-method coefficients (min/median/max).
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}.
|
| 57 |
+
edit_index (int | None): index into custom_resources to edit; None for a new batch.
|
| 58 |
+
preset_resources (list | None): restrict the group selector to this resource list
|
| 59 |
+
(used when launched from a group expander button).
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
None
|
| 63 |
+
"""
|
| 64 |
+
if edit_index is not None and not st.session_state.get(f"_dlg_initialized_{edit_index}"):
|
| 65 |
+
for key in list(st.session_state.keys()):
|
| 66 |
+
if key.startswith(("_dlg_existing_enable_",
|
| 67 |
+
"_dlg_existing_min_", "_dlg_existing_med_", "_dlg_existing_max_",
|
| 68 |
+
"_dlg_new_chk_", "_dlg_new_min_", "_dlg_new_med_", "_dlg_new_max_")):
|
| 69 |
+
del st.session_state[key]
|
| 70 |
+
st.session_state[f"_dlg_initialized_{edit_index}"] = True
|
| 71 |
+
|
| 72 |
+
standard_names = standard_resource_names()
|
| 73 |
+
existing_names = {
|
| 74 |
+
b["name"] for i, b in enumerate(st.session_state.custom_resources)
|
| 75 |
+
if i != edit_index
|
| 76 |
+
}
|
| 77 |
+
existing = st.session_state.custom_resources[edit_index] if edit_index is not None else {}
|
| 78 |
+
|
| 79 |
+
resource_to_group = get_resource_to_group()
|
| 80 |
+
|
| 81 |
+
# If opened from a specific resource-group expander ("+ Add resource" inline
|
| 82 |
+
# button), restrict the reference-resource choice to that group's resources
|
| 83 |
+
# only — the user picked the group by clicking that button.
|
| 84 |
+
group_options = preset_resources if preset_resources else resource_order
|
| 85 |
+
if preset_resources:
|
| 86 |
+
physical_group = resource_to_group.get(preset_resources[0], "")
|
| 87 |
+
st.markdown(
|
| 88 |
+
f'<p class="caption-input">Adding a new resource batch in the <strong>{physical_group}</strong> group.</p>',
|
| 89 |
+
unsafe_allow_html=True,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
st.markdown('<p class="dialog-title">1 — Name & group</p>', unsafe_allow_html=True)
|
| 93 |
+
|
| 94 |
+
col_name, col_group = st.columns(2)
|
| 95 |
+
with col_name:
|
| 96 |
+
batch_name = st.text_input(
|
| 97 |
+
"Resource name *",
|
| 98 |
+
value=existing.get("name", ""),
|
| 99 |
+
placeholder="e.g. Degraded forest lands – North",
|
| 100 |
+
help="It can't match the name of an existing resource.",
|
| 101 |
+
key="label-input-name"
|
| 102 |
+
)
|
| 103 |
+
with col_group:
|
| 104 |
+
default_group_idx = group_options.index(existing["group"]) if existing.get("group") in group_options else 0
|
| 105 |
+
batch_group = st.selectbox(
|
| 106 |
+
"Resource group *",
|
| 107 |
+
options=group_options,
|
| 108 |
+
index=default_group_idx,
|
| 109 |
+
help="Determines the physical unit of this resource.",
|
| 110 |
+
disabled=bool(preset_resources) and len(group_options) == 1,
|
| 111 |
+
key="label-input-group"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
group = resource_to_group.get(batch_group)
|
| 115 |
+
unit = resource_unit_map.get(group, "?")
|
| 116 |
+
|
| 117 |
+
st.markdown(f'<p class="caption">Unit for this group: <strong>{unit}</strong></p>', unsafe_allow_html=True)
|
| 118 |
+
|
| 119 |
+
st.markdown('<p class="dialog-title">2 — Available quantity</p>', unsafe_allow_html=True)
|
| 120 |
+
|
| 121 |
+
batch_amount = st.number_input(
|
| 122 |
+
f"Available amount ({unit})",
|
| 123 |
+
min_value=0.0,
|
| 124 |
+
format="%.2f",
|
| 125 |
+
value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None,
|
| 126 |
+
help=f"Total quantity available for 1 year, in {unit}.",
|
| 127 |
+
key="label-input-amount",
|
| 128 |
+
placeholder="0.0"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
st.markdown('<p class="dialog-title">3 — Existing CDR methods for this resource</p>', unsafe_allow_html=True)
|
| 132 |
+
|
| 133 |
+
method_configs = {}
|
| 134 |
+
|
| 135 |
+
all_related_methods = {}
|
| 136 |
+
for cdr_method, resource in cost_sliders.items():
|
| 137 |
+
for name, coeff in resource.items():
|
| 138 |
+
if batch_group == strip_unit_suffix(name):
|
| 139 |
+
all_related_methods[cdr_method] = {
|
| 140 |
+
"min": coeff["min"],
|
| 141 |
+
"median": coeff["median"],
|
| 142 |
+
"max": coeff["max"],
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Split into primary (selectable) and secondary (auto-linked in Tab 3)
|
| 146 |
+
primary_related = {
|
| 147 |
+
m: coeff for m, coeff in all_related_methods.items()
|
| 148 |
+
if (m, batch_group) not in secondary_resources
|
| 149 |
+
}
|
| 150 |
+
secondary_related = {
|
| 151 |
+
m: coeff for m, coeff in all_related_methods.items()
|
| 152 |
+
if (m, batch_group) in secondary_resources
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
primary_by_group = {}
|
| 156 |
+
for m, coeff in primary_related.items():
|
| 157 |
+
method_group = get_method_group(m)
|
| 158 |
+
primary_by_group.setdefault(method_group, {})[m] = coeff
|
| 159 |
+
|
| 160 |
+
if not primary_by_group:
|
| 161 |
+
st.markdown('<p class="caption">No primary CDR methods use this resource directly.</p>', unsafe_allow_html=True)
|
| 162 |
+
|
| 163 |
+
for method_group, methods in primary_by_group.items():
|
| 164 |
+
with st.expander(f"**{method_group}**", expanded=False):
|
| 165 |
+
for m, coeff in methods.items():
|
| 166 |
+
|
| 167 |
+
already_selected = m in existing.get("methods", {})
|
| 168 |
+
|
| 169 |
+
if f"_dlg_existing_enable_{m}" not in st.session_state:
|
| 170 |
+
st.session_state[f"_dlg_existing_enable_{m}"] = already_selected
|
| 171 |
+
|
| 172 |
+
enabled = st.checkbox(
|
| 173 |
+
f"{m}",
|
| 174 |
+
key=f"_dlg_existing_enable_{m}",
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
if not enabled:
|
| 178 |
+
continue
|
| 179 |
+
|
| 180 |
+
existing_coeff = existing.get("methods", {}).get(m, coeff)
|
| 181 |
+
|
| 182 |
+
st.warning("Modifying these values only affects this resource batch: the base method coefficients are unchanged.")
|
| 183 |
+
col_min, col_med, col_max = st.columns(3)
|
| 184 |
+
with col_min:
|
| 185 |
+
e_min = st.number_input(
|
| 186 |
+
f"Min ({unit}/{CO2_UNIT})",
|
| 187 |
+
min_value=0.0,
|
| 188 |
+
format="%.10g",
|
| 189 |
+
value=float(existing_coeff["min"]),
|
| 190 |
+
key=f"_dlg_existing_min_{m}",
|
| 191 |
+
)
|
| 192 |
+
with col_med:
|
| 193 |
+
e_med = st.number_input(
|
| 194 |
+
f"Median ({unit}/{CO2_UNIT})",
|
| 195 |
+
min_value=0.0,
|
| 196 |
+
format="%.10g",
|
| 197 |
+
value=float(existing_coeff["median"]),
|
| 198 |
+
key=f"_dlg_existing_med_{m}",
|
| 199 |
+
)
|
| 200 |
+
with col_max:
|
| 201 |
+
e_max = st.number_input(
|
| 202 |
+
f"Max ({unit}/{CO2_UNIT})",
|
| 203 |
+
min_value=0.0,
|
| 204 |
+
format="%.10g",
|
| 205 |
+
value=float(existing_coeff["max"]),
|
| 206 |
+
key=f"_dlg_existing_max_{m}",
|
| 207 |
+
)
|
| 208 |
+
method_configs[m] = {
|
| 209 |
+
"min": e_min,
|
| 210 |
+
"median": e_med,
|
| 211 |
+
"max": e_max,
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if secondary_related:
|
| 215 |
+
names = "\n".join(f"- **{m}**" for m in secondary_related)
|
| 216 |
+
st.info(
|
| 217 |
+
f"""The following methods use this resource as **secondary** and may be assigned later in the **Resource Coefficients** tab:\n{names}
|
| 218 |
+
""",
|
| 219 |
+
icon=":material/info:",
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
st.markdown('<p class="dialog-title">4 — Add additional CDR methods</p>', unsafe_allow_html=True)
|
| 224 |
+
st.markdown(f'<p class="caption">Select additional methods and enter their coefficients in {unit}/{CO2_UNIT}.</p>', unsafe_allow_html=True)
|
| 225 |
+
|
| 226 |
+
already_used = set(all_related_methods.keys())
|
| 227 |
+
|
| 228 |
+
for group_name, methods in method_groups.items():
|
| 229 |
+
available_methods = [
|
| 230 |
+
m for m in methods
|
| 231 |
+
if m not in already_used
|
| 232 |
+
]
|
| 233 |
+
if not available_methods:
|
| 234 |
+
continue
|
| 235 |
+
with st.expander(f"**{group_name}**", expanded=False):
|
| 236 |
+
for m in available_methods:
|
| 237 |
+
enabled = st.checkbox(
|
| 238 |
+
f"**{m}**",
|
| 239 |
+
key=f"_dlg_new_chk_{m}",
|
| 240 |
+
)
|
| 241 |
+
if enabled:
|
| 242 |
+
col_min, col_med, col_max = st.columns(3)
|
| 243 |
+
with col_min:
|
| 244 |
+
v_min = st.number_input(
|
| 245 |
+
f"Min ({unit}/{CO2_UNIT})",
|
| 246 |
+
min_value=0.0,
|
| 247 |
+
value=None,
|
| 248 |
+
format="%.10g",
|
| 249 |
+
key=f"_dlg_new_min_{m}",
|
| 250 |
+
placeholder="0.0000",
|
| 251 |
+
)
|
| 252 |
+
with col_med:
|
| 253 |
+
v_med = st.number_input(
|
| 254 |
+
f"Median ({unit}/{CO2_UNIT})",
|
| 255 |
+
min_value=0.0,
|
| 256 |
+
value=None,
|
| 257 |
+
format="%.10g",
|
| 258 |
+
key=f"_dlg_new_med_{m}",
|
| 259 |
+
placeholder="0.0000",
|
| 260 |
+
)
|
| 261 |
+
with col_max:
|
| 262 |
+
v_max = st.number_input(
|
| 263 |
+
f"Ma ({unit}/{CO2_UNIT})",
|
| 264 |
+
min_value=0.0,
|
| 265 |
+
value=None,
|
| 266 |
+
format="%.10g",
|
| 267 |
+
key=f"_dlg_new_max_{m}",
|
| 268 |
+
placeholder="0.0000",
|
| 269 |
+
)
|
| 270 |
+
method_configs[m] = {
|
| 271 |
+
"min": v_min,
|
| 272 |
+
"median": v_med,
|
| 273 |
+
"max": v_max,
|
| 274 |
+
}
|
| 275 |
+
error_placeholder = st.empty()
|
| 276 |
+
col_save, col_cancel = st.columns([2, 2])
|
| 277 |
+
|
| 278 |
+
with col_cancel:
|
| 279 |
+
if st.button("Cancel", width='stretch', key="btn-reset-cancel"):
|
| 280 |
+
st.rerun()
|
| 281 |
+
|
| 282 |
+
def _validate():
|
| 283 |
+
errors = []
|
| 284 |
+
if not batch_name.strip():
|
| 285 |
+
errors.append("Resource name is required.")
|
| 286 |
+
elif batch_name.strip() in standard_names:
|
| 287 |
+
errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.")
|
| 288 |
+
elif batch_name.strip() in existing_names:
|
| 289 |
+
errors.append(f"**{batch_name}** already exists. Choose a different name.")
|
| 290 |
+
if not batch_amount or batch_amount <= 0:
|
| 291 |
+
errors.append("Available amount must be greater than 0.")
|
| 292 |
+
# if not method_configs:
|
| 293 |
+
# errors.append("Select at least one CDR method.")
|
| 294 |
+
for m, c in method_configs.items():
|
| 295 |
+
if any(v is None for v in [c["min"], c["median"], c["max"]]):
|
| 296 |
+
errors.append(f"**{m}**: all coefficient fields (min, median, max) are required.")
|
| 297 |
+
continue
|
| 298 |
+
if c["min"] > c["max"]:
|
| 299 |
+
errors.append(f"**{m}**: min ({c['min']}) cannot be greater than max ({c['max']}).")
|
| 300 |
+
if not (c["min"] <= c["median"] <= c["max"]):
|
| 301 |
+
errors.append(f"**{m}**: median must be between min and max.")
|
| 302 |
+
return errors
|
| 303 |
+
|
| 304 |
+
with col_save:
|
| 305 |
+
if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key="btn-new-add"):
|
| 306 |
+
errors = _validate()
|
| 307 |
+
if errors:
|
| 308 |
+
with error_placeholder.container():
|
| 309 |
+
for e in errors:
|
| 310 |
+
st.error(e, icon=":material/error:")
|
| 311 |
+
else:
|
| 312 |
+
batch = {
|
| 313 |
+
"name": batch_name.strip(),
|
| 314 |
+
"group": batch_group,
|
| 315 |
+
"amount": float(batch_amount), # always store as float
|
| 316 |
+
"unit": resource_unit_map.get(resource_to_group.get(batch_group), "?"),
|
| 317 |
+
"methods": method_configs,
|
| 318 |
+
}
|
| 319 |
+
if edit_index is not None:
|
| 320 |
+
st.session_state.custom_resources[edit_index] = batch
|
| 321 |
+
st.session_state.pop(f"custom_amount_{edit_index}", None)
|
| 322 |
+
else:
|
| 323 |
+
new_idx = len(st.session_state.custom_resources)
|
| 324 |
+
st.session_state.custom_resources.append(batch)
|
| 325 |
+
st.session_state.pop(f"custom_amount_{new_idx}", None)
|
| 326 |
+
st.session_state.pop("latest_result", None)
|
| 327 |
+
st.rerun()
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def to_excel_bytes(batches: list) -> bytes:
|
| 332 |
+
"""Serialise all custom resource batches to an Excel file (one row per batch × method).
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
batches (list): list of custom batch dicts with keys name, group, amount, unit, methods.
|
| 336 |
+
|
| 337 |
+
Returns:
|
| 338 |
+
bytes: raw bytes of the .xlsx workbook.
|
| 339 |
+
"""
|
| 340 |
+
rows = []
|
| 341 |
+
for batch in batches:
|
| 342 |
+
for m, coefs in batch["methods"].items():
|
| 343 |
+
rows.append({
|
| 344 |
+
"name": batch["name"],
|
| 345 |
+
"group": batch["group"],
|
| 346 |
+
"amount": float(batch["amount"]),
|
| 347 |
+
"unit": batch["unit"],
|
| 348 |
+
"method": m,
|
| 349 |
+
"min": float(coefs["min"]),
|
| 350 |
+
"median": float(coefs["median"]),
|
| 351 |
+
"max": float(coefs["max"]),
|
| 352 |
+
})
|
| 353 |
+
excel_buffer = BytesIO()
|
| 354 |
+
with pd.ExcelWriter(excel_buffer, engine="openpyxl") as writer:
|
| 355 |
+
pd.DataFrame(rows).to_excel(writer, index=False, sheet_name="custom_resources")
|
| 356 |
+
return excel_buffer.getvalue()
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def _parse_custom_excel(file) -> list:
|
| 360 |
+
"""Parse a custom resource Excel file into a list of batch dicts.
|
| 361 |
+
|
| 362 |
+
Expects columns: name, group, amount, unit, method, min, median, max.
|
| 363 |
+
|
| 364 |
+
Args:
|
| 365 |
+
file: file-like object pointing to an .xlsx workbook.
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
list[dict]: one dict per unique batch name with keys
|
| 369 |
+
name, group, amount, unit, methods.
|
| 370 |
+
|
| 371 |
+
Raises:
|
| 372 |
+
ValueError: if required columns are missing.
|
| 373 |
+
"""
|
| 374 |
+
df = pd.read_excel(file, sheet_name=0)
|
| 375 |
+
required = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
|
| 376 |
+
if not required.issubset(df.columns):
|
| 377 |
+
raise ValueError(f"Missing columns: {required - set(df.columns)}")
|
| 378 |
+
batches = []
|
| 379 |
+
for batch_name in df["name"].unique():
|
| 380 |
+
df_b = df[df["name"] == batch_name]
|
| 381 |
+
methods = {
|
| 382 |
+
row["method"]: {
|
| 383 |
+
"min": float(row["min"]),
|
| 384 |
+
"median": float(row["median"]),
|
| 385 |
+
"max": float(row["max"]),
|
| 386 |
+
}
|
| 387 |
+
for _, row in df_b.iterrows()
|
| 388 |
+
}
|
| 389 |
+
group_name = str(df_b["group"].iloc[0])
|
| 390 |
+
resource_to_group = get_resource_to_group()
|
| 391 |
+
batches.append({
|
| 392 |
+
"name": str(batch_name),
|
| 393 |
+
"group": group_name,
|
| 394 |
+
"amount": float(df_b["amount"].iloc[0]),
|
| 395 |
+
"unit": resource_unit_map.get(resource_to_group.get(group_name), "?"),
|
| 396 |
+
"methods": methods,
|
| 397 |
+
})
|
| 398 |
+
return batches
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
@st.dialog("Load custom resource batches", width="large")
|
| 403 |
+
def dialog_load_custom_batch():
|
| 404 |
+
"""Dialog for importing custom resource batches from an Excel file.
|
| 405 |
+
|
| 406 |
+
Reads an .xlsx file produced by to_excel_bytes, previews each batch's
|
| 407 |
+
status (new / overwrite / conflict), and merges them into session state
|
| 408 |
+
on confirmation.
|
| 409 |
+
|
| 410 |
+
Returns:
|
| 411 |
+
None
|
| 412 |
+
"""
|
| 413 |
+
st.markdown('<p class="caption">Import an Excel file previously exported from the <strong>Custom resource</strong> section.</p>', unsafe_allow_html=True)
|
| 414 |
+
|
| 415 |
+
uploaded = st.file_uploader("Upload Excel file", type=["xlsx"], key="inputs_loader_custom")
|
| 416 |
+
if uploaded is None:
|
| 417 |
+
return
|
| 418 |
+
|
| 419 |
+
try:
|
| 420 |
+
new_batches = _parse_custom_excel(uploaded)
|
| 421 |
+
except Exception as e:
|
| 422 |
+
st.error(f"Error reading file: {e}")
|
| 423 |
+
return
|
| 424 |
+
|
| 425 |
+
standard_names = standard_resource_names()
|
| 426 |
+
existing_names = {b["name"] for b in st.session_state.custom_resources}
|
| 427 |
+
|
| 428 |
+
previews = []
|
| 429 |
+
for batch in new_batches:
|
| 430 |
+
if batch["name"] in standard_names:
|
| 431 |
+
status = "❌ Conflicts with standard resource — will be skipped"
|
| 432 |
+
elif batch["name"] in existing_names:
|
| 433 |
+
status = "⚠️ Already exists — will be overwritten"
|
| 434 |
+
else:
|
| 435 |
+
status = "✅ New"
|
| 436 |
+
previews.append({
|
| 437 |
+
"Resource name": batch["name"],
|
| 438 |
+
"Group": batch["group"],
|
| 439 |
+
"Amount": f"{batch["amount"]} {resource_unit_map.get(batch["group"], "")}",
|
| 440 |
+
"Methods": len(batch["methods"]),
|
| 441 |
+
"Status": status,
|
| 442 |
+
})
|
| 443 |
+
st.dataframe(pd.DataFrame(previews), width='stretch', hide_index=True)
|
| 444 |
+
|
| 445 |
+
if st.button("↩️ Import all batches", type="primary", width='stretch'):
|
| 446 |
+
imported = 0
|
| 447 |
+
for batch in new_batches:
|
| 448 |
+
if batch["name"] in standard_names:
|
| 449 |
+
continue
|
| 450 |
+
if batch["name"] in existing_names:
|
| 451 |
+
idx = next(i for i, b in enumerate(st.session_state.custom_resources)
|
| 452 |
+
if b["name"] == batch["name"])
|
| 453 |
+
st.session_state.custom_resources[idx] = batch
|
| 454 |
+
else:
|
| 455 |
+
st.session_state.custom_resources.append(batch)
|
| 456 |
+
imported += 1
|
| 457 |
+
st.session_state.pop("latest_result", None)
|
| 458 |
+
st.success(f"Imported {imported} batch(es).")
|
| 459 |
+
st.rerun()
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def _render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
|
| 464 |
+
"""Render a single custom batch inside its resource-group expander."""
|
| 465 |
+
n_methods = len(batch["methods"])
|
| 466 |
+
amount_val = float(batch["amount"])
|
| 467 |
+
batch["unit"] = unit # keep in sync for Excel export
|
| 468 |
+
|
| 469 |
+
# st.markdown(f"🆕 **{batch['name']}** — CDR methods: **{n_methods}**")
|
| 470 |
+
with st.container(border=True):
|
| 471 |
+
key_amount = f"custom_amount_{global_idx}"
|
| 472 |
+
if key_amount not in st.session_state:
|
| 473 |
+
st.session_state[key_amount] = amount_val
|
| 474 |
+
|
| 475 |
+
col_amount, _, _, _ = st.columns(4)
|
| 476 |
+
with col_amount:
|
| 477 |
+
new_amount = st.number_input(
|
| 478 |
+
f"{batch['name']}",
|
| 479 |
+
min_value=0.0,
|
| 480 |
+
format="%.2f",
|
| 481 |
+
key=key_amount,
|
| 482 |
+
)
|
| 483 |
+
if new_amount != amount_val:
|
| 484 |
+
st.session_state.custom_resources[global_idx]["amount"] = float(new_amount)
|
| 485 |
+
st.session_state.pop("latest_result", None)
|
| 486 |
+
|
| 487 |
+
col_min = f"Min ({unit}/MtCO₂)"
|
| 488 |
+
col_med = f"Median ({unit}/MtCO₂)"
|
| 489 |
+
col_max = f"Max ({unit}/MtCO₂)"
|
| 490 |
+
rows = [
|
| 491 |
+
{
|
| 492 |
+
"Method": m,
|
| 493 |
+
col_min: float(c["min"]),
|
| 494 |
+
col_med: float(c["median"]),
|
| 495 |
+
col_max: float(c["max"]),
|
| 496 |
+
}
|
| 497 |
+
for m, c in batch["methods"].items()
|
| 498 |
+
]
|
| 499 |
+
st.dataframe(
|
| 500 |
+
pd.DataFrame(rows),
|
| 501 |
+
width='stretch',
|
| 502 |
+
hide_index=True,
|
| 503 |
+
key=f"batch_editor_{global_idx}",
|
| 504 |
+
column_config={
|
| 505 |
+
"Method": st.column_config.TextColumn(disabled=True),
|
| 506 |
+
col_min: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
|
| 507 |
+
col_med: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
|
| 508 |
+
col_max: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
|
| 509 |
+
},
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
with st.container(key=f"container-button-{global_idx}"):
|
| 513 |
+
if st.button("Delete", key=f"del_batch_{global_idx}",
|
| 514 |
+
type="primary", icon=":material/delete:"):
|
| 515 |
+
st.session_state.custom_resources.pop(global_idx)
|
| 516 |
+
st.session_state.pop("latest_result", None)
|
| 517 |
+
st.rerun()
|
| 518 |
+
if st.button("Edit", key=f"edit_batch_{global_idx}",
|
| 519 |
+
type="secondary", icon=":material/edit:"):
|
| 520 |
+
dialog_add_custom_resource(cost_sliders, edit_index=global_idx)
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
def panel_custom_resources_controls(cost_sliders):
|
| 525 |
+
"""Buttons to add / load / download custom resource batches (no batch display here)."""
|
| 526 |
+
init_custom_resources()
|
| 527 |
+
|
| 528 |
+
st.markdown('<p class="crra-sub-heading">➕ New resource</p>', unsafe_allow_html=True)
|
| 529 |
+
st.markdown(
|
| 530 |
+
'<p class="caption"> Add sub-batches for any resource group and manually assign them to specific CDR methods.</p>',
|
| 531 |
+
unsafe_allow_html=True,
|
| 532 |
+
)
|
| 533 |
+
batches = st.session_state.custom_resources
|
| 534 |
+
|
| 535 |
+
with st.container(key="container-btn"):
|
| 536 |
+
if st.button("New resource", type="tertiary", icon=":material/add:", key="btn-new-resource"):
|
| 537 |
+
dialog_add_custom_resource(cost_sliders)
|
| 538 |
+
if st.button("Load from Excel", type="tertiary", icon=":material/arrow_upload_progress:", key="btn-load"):
|
| 539 |
+
dialog_load_custom_batch()
|
| 540 |
+
if batches:
|
| 541 |
+
st.download_button(
|
| 542 |
+
"Download new resources",
|
| 543 |
+
data=to_excel_bytes(batches),
|
| 544 |
+
file_name="new_resources.xlsx",
|
| 545 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 546 |
+
icon=":material/download:",
|
| 547 |
+
key="download_button_save_custom",
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def render_tab(cost_sliders):
|
| 553 |
+
"""Render the Resource Inputs tab.
|
| 554 |
+
|
| 555 |
+
Shows per-resource number inputs grouped by resource group, custom batch
|
| 556 |
+
controls, an upload/download template workflow, and a summary table. Also
|
| 557 |
+
auto-deactivates CDR methods whose primary resources are all disabled.
|
| 558 |
+
|
| 559 |
+
Args:
|
| 560 |
+
cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}.
|
| 561 |
+
|
| 562 |
+
Returns:
|
| 563 |
+
None
|
| 564 |
+
"""
|
| 565 |
+
if "resource_caps" not in st.session_state:
|
| 566 |
+
st.session_state.resource_caps = {}
|
| 567 |
+
for r in resource_order:
|
| 568 |
+
if r not in st.session_state.resource_caps:
|
| 569 |
+
st.session_state.resource_caps[r] = 0.0
|
| 570 |
+
init_custom_resources()
|
| 571 |
+
|
| 572 |
+
st.markdown('<h1 class="crra-heading">Resource inputs</h1>', unsafe_allow_html=True)
|
| 573 |
+
show_bar()
|
| 574 |
+
|
| 575 |
+
st.markdown('<p class="objective_title">OBJECTIVE</p>', unsafe_allow_html=True)
|
| 576 |
+
st.markdown(
|
| 577 |
+
'<p class="text_with_border">This section lets you define the <strong>total available '
|
| 578 |
+
'quantities of each resource</strong> for the optimization model.</p>',
|
| 579 |
+
unsafe_allow_html=True,
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
with st.container(key="warning_resources"):
|
| 583 |
+
st.warning(
|
| 584 |
+
"Resources with zero availability will block portfolio feasibility if required by active methods.",
|
| 585 |
+
icon=":material/warning:",
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
st.markdown('<p class="crra-sub-heading">Controls</p>', unsafe_allow_html=True)
|
| 589 |
+
|
| 590 |
+
if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
|
| 591 |
+
for r in st.session_state.resource_caps:
|
| 592 |
+
st.session_state.resource_caps[r] = 0.0
|
| 593 |
+
st.session_state.custom_resources = []
|
| 594 |
+
st.session_state.enabled_methods = {}
|
| 595 |
+
st.success("All resource values reset to 0.")
|
| 596 |
+
st.rerun()
|
| 597 |
+
|
| 598 |
+
st.markdown('<p class="crra-sub-sub-heading">Import resource values</p>', unsafe_allow_html=True)
|
| 599 |
+
col_template, col_upload = st.columns([1, 1])
|
| 600 |
+
with col_template:
|
| 601 |
+
st.markdown("<div class='caption'><p>Use the template below to fill in your resource "
|
| 602 |
+
"availability values and import them directly into the tool.</p></div>",
|
| 603 |
+
unsafe_allow_html=True)
|
| 604 |
+
with open("data/resource_template.xlsx", "rb") as f:
|
| 605 |
+
st.download_button(
|
| 606 |
+
label="Download Excel Template",
|
| 607 |
+
data=f,
|
| 608 |
+
file_name="resource_template.xlsx",
|
| 609 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 610 |
+
icon=":material/download:",
|
| 611 |
+
key="download_button_resource_template"
|
| 612 |
+
)
|
| 613 |
+
with col_upload:
|
| 614 |
+
uploaded_file = st.file_uploader(
|
| 615 |
+
"Upload your Excel file", type="xlsx",
|
| 616 |
+
key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}"
|
| 617 |
+
)
|
| 618 |
+
if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
|
| 619 |
+
try:
|
| 620 |
+
df = pd.read_excel(uploaded_file)
|
| 621 |
+
required_cols = {"Resource", "Available Amount"}
|
| 622 |
+
if not required_cols.issubset(df.columns):
|
| 623 |
+
raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
|
| 624 |
+
for r in st.session_state.resource_caps:
|
| 625 |
+
st.session_state.resource_caps[r] = 0.0
|
| 626 |
+
st.session_state[f"cap_{r}"] = 0.0
|
| 627 |
+
for _, row in df.iterrows():
|
| 628 |
+
r = row["Resource"]
|
| 629 |
+
val = row["Available Amount"]
|
| 630 |
+
if r not in st.session_state.resource_caps:
|
| 631 |
+
continue
|
| 632 |
+
try:
|
| 633 |
+
val = 0.0 if pd.isna(val) else float(val)
|
| 634 |
+
except (ValueError, TypeError):
|
| 635 |
+
val = 0.0
|
| 636 |
+
st.session_state.resource_caps[r] = val
|
| 637 |
+
st.session_state[f"cap_{r}"] = val
|
| 638 |
+
st.session_state["resource_last_loaded_file"] = uploaded_file.name
|
| 639 |
+
st.success("Resource values imported successfully.", icon="✅")
|
| 640 |
+
for key in ["excel_loaded_file", "scenario_name_suggestion",
|
| 641 |
+
"scenario_loaded_file", "input_file_name_suggestion", "latest_result"]:
|
| 642 |
+
st.session_state.pop(key, None)
|
| 643 |
+
st.rerun()
|
| 644 |
+
except Exception as e:
|
| 645 |
+
st.error(f"Error importing resource values: {e}")
|
| 646 |
+
elif st.session_state.get("resource_last_loaded_file"):
|
| 647 |
+
st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="✅")
|
| 648 |
+
|
| 649 |
+
# Standard resource inputs
|
| 650 |
+
st.markdown('<p class="crra-sub-heading">Set available resources for one year</p>', unsafe_allow_html=True)
|
| 651 |
+
|
| 652 |
+
# Build a lookup: for each CDR method, which resources are "primary" (mandatory)?
|
| 653 |
+
# Secondary resources are optional add-ons — a method can still run without them.
|
| 654 |
+
# A method can only run in the optimisation if at least one primary resource is available.
|
| 655 |
+
method_to_primary_resources = {}
|
| 656 |
+
for cdr_method, resource_coeffs in cost_sliders.items():
|
| 657 |
+
for name in resource_coeffs:
|
| 658 |
+
r_clean = strip_unit_suffix(name)
|
| 659 |
+
if (cdr_method, r_clean) not in secondary_resources:
|
| 660 |
+
method_to_primary_resources.setdefault(cdr_method, []).append(r_clean)
|
| 661 |
+
|
| 662 |
+
for r in method_to_primary_resources:
|
| 663 |
+
pass # no-op, kept for clarity of intent below
|
| 664 |
+
|
| 665 |
+
resource_to_group = get_resource_to_group()
|
| 666 |
+
|
| 667 |
+
for group, rlist in resource_groups.items():
|
| 668 |
+
unit = resource_unit_map.get(group, "")
|
| 669 |
+
with st.expander(f"**{group}** ({unit})", expanded=True):
|
| 670 |
+
|
| 671 |
+
cols = st.columns(4)
|
| 672 |
+
|
| 673 |
+
for i, r in enumerate(rlist):
|
| 674 |
+
|
| 675 |
+
tooltip = resource_tooltips.get(r, None)
|
| 676 |
+
name_clean = r.split(" (")[0]
|
| 677 |
+
|
| 678 |
+
with cols[i % 4]:
|
| 679 |
+
|
| 680 |
+
render_resource_input(name_clean, r, tooltip=tooltip)
|
| 681 |
+
|
| 682 |
+
related_methods = {}
|
| 683 |
+
related_by_group = {}
|
| 684 |
+
|
| 685 |
+
for cdr_method, resource in cost_sliders.items():
|
| 686 |
+
for name, coeff in resource.items():
|
| 687 |
+
if r == strip_unit_suffix(name):
|
| 688 |
+
related_methods[cdr_method] = coeff
|
| 689 |
+
|
| 690 |
+
for m in related_methods:
|
| 691 |
+
related_by_group.setdefault(get_method_group(m), []).append(m)
|
| 692 |
+
|
| 693 |
+
related_method_names = list(related_methods.keys())
|
| 694 |
+
secondary_for_r = [m for m in related_method_names if (m, r) in secondary_resources]
|
| 695 |
+
primary_for_r = [m for m in related_method_names if (m, r) not in secondary_resources]
|
| 696 |
+
|
| 697 |
+
if r not in st.session_state.enabled_methods:
|
| 698 |
+
st.session_state.enabled_methods[r] = {}
|
| 699 |
+
for m in related_method_names:
|
| 700 |
+
st.session_state.enabled_methods[r].setdefault(m, True)
|
| 701 |
+
popover_key_prefix = f"pop_{r}"
|
| 702 |
+
|
| 703 |
+
with st.popover(
|
| 704 |
+
f":material/tune: Related CDR Methods",
|
| 705 |
+
width='stretch',
|
| 706 |
+
):
|
| 707 |
+
for method_group, methods in related_by_group.items():
|
| 708 |
+
primary_in_group = [m for m in methods if m in primary_for_r]
|
| 709 |
+
if not primary_in_group:
|
| 710 |
+
continue
|
| 711 |
+
st.markdown(f"*{method_group}*")
|
| 712 |
+
for m in primary_in_group:
|
| 713 |
+
widget_key = f"{popover_key_prefix}_{m}"
|
| 714 |
+
if widget_key not in st.session_state:
|
| 715 |
+
st.session_state[widget_key] = st.session_state.enabled_methods[r][m]
|
| 716 |
+
|
| 717 |
+
checked = st.checkbox(m, key=widget_key)
|
| 718 |
+
st.session_state.enabled_methods[r][m] = checked
|
| 719 |
+
|
| 720 |
+
if secondary_for_r:
|
| 721 |
+
st.caption(
|
| 722 |
+
"Secondary methods that can be added in the **Resource Coefficient** tab:\n\n"
|
| 723 |
+
+ "\n".join(f"- {m}" for m in secondary_for_r)
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
for m in secondary_for_r:
|
| 727 |
+
st.session_state.enabled_methods[r][m] = True
|
| 728 |
+
|
| 729 |
+
# Custom batches for this group
|
| 730 |
+
group_unit = resource_unit_map.get(group, "")
|
| 731 |
+
group_batches = [
|
| 732 |
+
(idx, b) for idx, b in enumerate(st.session_state.get("custom_resources", []))
|
| 733 |
+
if resource_to_group.get(b["group"]) == group
|
| 734 |
+
]
|
| 735 |
+
for global_idx, batch in group_batches:
|
| 736 |
+
_render_custom_batch_inline(global_idx, batch, cost_sliders, group_unit)
|
| 737 |
+
|
| 738 |
+
_, col_button = st.columns([3,1])
|
| 739 |
+
with col_button:
|
| 740 |
+
if st.button(
|
| 741 |
+
"Add resource", key=f"simple-btn-resource-{group}",
|
| 742 |
+
type="tertiary", icon=":material/add:", width='stretch'
|
| 743 |
+
):
|
| 744 |
+
dialog_add_custom_resource(cost_sliders, preset_resources=rlist)
|
| 745 |
+
|
| 746 |
+
# ── AUTO-DISABLE LOGIC ────────────────────────────────────────────────────
|
| 747 |
+
# A CDR method cannot run in the optimisation if it has no usable resources.
|
| 748 |
+
# This block detects that situation automatically and deactivates the method
|
| 749 |
+
# in the Method Constraints tab — so the optimisation never fails silently.
|
| 750 |
+
#
|
| 751 |
+
# The logic runs in three steps every time this tab renders:
|
| 752 |
+
# 1. Count how many active primary resources each method still has.
|
| 753 |
+
# 2. Re-enable any method that was auto-disabled and now has resources again
|
| 754 |
+
# (e.g. the user re-entered a value). Methods the user manually disabled
|
| 755 |
+
# are stored separately and are NOT touched here.
|
| 756 |
+
# 3. Disable any method whose resource count just dropped to zero, and show
|
| 757 |
+
# a notification so the user knows what changed.
|
| 758 |
+
# ─────────────────────────────────────────────────────────────────────────
|
| 759 |
+
|
| 760 |
+
# Step 1 – count active primary resources per method (secondary resources are
|
| 761 |
+
# optional extras; they do not determine whether a method can run).
|
| 762 |
+
method_active_resource_count = {}
|
| 763 |
+
for r, methods in st.session_state.enabled_methods.items():
|
| 764 |
+
for m, ok in methods.items():
|
| 765 |
+
if (m, r) in secondary_resources:
|
| 766 |
+
continue
|
| 767 |
+
method_active_resource_count.setdefault(m, 0)
|
| 768 |
+
if ok:
|
| 769 |
+
method_active_resource_count[m] += 1
|
| 770 |
+
|
| 771 |
+
# Custom batches also count as active resources for the methods they are
|
| 772 |
+
# assigned to, even when the standard reference resource is toggled off.
|
| 773 |
+
for batch in st.session_state.get("custom_resources", []):
|
| 774 |
+
if float(batch.get("amount") or 0) <= 0:
|
| 775 |
+
continue
|
| 776 |
+
for m_name in batch.get("methods", {}).keys():
|
| 777 |
+
method_active_resource_count.setdefault(m_name, 0)
|
| 778 |
+
method_active_resource_count[m_name] += 1
|
| 779 |
+
|
| 780 |
+
methods_with_no_active_resource = [
|
| 781 |
+
m for m in method_to_primary_resources
|
| 782 |
+
if method_active_resource_count.get(m, 0) == 0
|
| 783 |
+
]
|
| 784 |
+
|
| 785 |
+
# Step 2 – re-enable methods that the tool previously auto-disabled but that
|
| 786 |
+
# now have at least one active resource (user restored a value or added a batch).
|
| 787 |
+
for m in list(st.session_state.auto_disabled_methods):
|
| 788 |
+
if method_active_resource_count.get(m, 0) > 0:
|
| 789 |
+
if m in st.session_state.constraints:
|
| 790 |
+
st.session_state.constraints[m]["active"] = True
|
| 791 |
+
toggle_key = f"toggle_{m}"
|
| 792 |
+
if toggle_key in st.session_state:
|
| 793 |
+
st.session_state[toggle_key] = True
|
| 794 |
+
st.session_state.auto_disabled_methods.discard(m)
|
| 795 |
+
|
| 796 |
+
# Step 3 – disable methods that now have zero active primary resources.
|
| 797 |
+
# We record which methods the tool disabled (auto_disabled_methods) so that
|
| 798 |
+
# step 2 can safely re-enable them later without touching user-chosen settings.
|
| 799 |
+
newly_disabled = []
|
| 800 |
+
for m in methods_with_no_active_resource:
|
| 801 |
+
if st.session_state.constraints.get(m, {}).get("active", True):
|
| 802 |
+
st.session_state.constraints.setdefault(m, {"active": True, "cap_type": "percent", "cap_value": 100})
|
| 803 |
+
st.session_state.constraints[m]["active"] = False
|
| 804 |
+
st.session_state.auto_disabled_methods.add(m)
|
| 805 |
+
newly_disabled.append(m)
|
| 806 |
+
toggle_key = f"toggle_{m}"
|
| 807 |
+
if toggle_key in st.session_state:
|
| 808 |
+
st.session_state[toggle_key] = False
|
| 809 |
+
|
| 810 |
+
if newly_disabled:
|
| 811 |
+
st.info(
|
| 812 |
+
"The following methods have no active resource left and were "
|
| 813 |
+
"automatically deactivated (see Method constraints tab): "
|
| 814 |
+
+ ", ".join(newly_disabled),
|
| 815 |
+
icon=":material/info:"
|
| 816 |
+
)
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
panel_custom_resources_controls(cost_sliders)
|
| 820 |
+
|
| 821 |
+
st.markdown('<p class="crra-sub-heading">Summary of available resources</p>', unsafe_allow_html=True)
|
| 822 |
+
|
| 823 |
+
resource_to_unit = get_resource_to_unit()
|
| 824 |
+
|
| 825 |
+
df_caps = pd.DataFrame.from_dict(
|
| 826 |
+
{r: st.session_state.resource_caps[r] for r in resource_order
|
| 827 |
+
if st.session_state.resource_caps.get(r, 0.0) > 0},
|
| 828 |
+
orient="index", columns=["Available Amount"],
|
| 829 |
+
).reindex(resource_order).dropna()
|
| 830 |
+
df_caps["Available Amount"] = [
|
| 831 |
+
f"{value:,.2f} {resource_to_unit.get(resource, '?')}"
|
| 832 |
+
for resource, value in df_caps["Available Amount"].items()
|
| 833 |
+
]
|
| 834 |
+
|
| 835 |
+
custom_rows = []
|
| 836 |
+
for batch in st.session_state.get("custom_resources", []):
|
| 837 |
+
group = resource_to_group.get(batch["group"])
|
| 838 |
+
unit = resource_unit_map.get(group, "?")
|
| 839 |
+
custom_rows.append({
|
| 840 |
+
"Resource": batch["name"],
|
| 841 |
+
"Available Amount": f"{float(batch['amount']):,.2f} {unit}",
|
| 842 |
+
})
|
| 843 |
+
|
| 844 |
+
if not df_caps.empty or custom_rows:
|
| 845 |
+
dfs = []
|
| 846 |
+
if not df_caps.empty:
|
| 847 |
+
df_std = df_caps.copy()
|
| 848 |
+
df_std["Type"] = "Standard"
|
| 849 |
+
dfs.append(df_std)
|
| 850 |
+
if custom_rows:
|
| 851 |
+
df_custom = pd.DataFrame(custom_rows).set_index("Resource")
|
| 852 |
+
df_custom["Type"] = "New"
|
| 853 |
+
dfs.append(df_custom)
|
| 854 |
+
df_all = pd.concat(dfs)[["Available Amount", "Type"]].reset_index().rename(columns={"index": "Resource"})
|
| 855 |
+
num_rows = len(df_all)
|
| 856 |
+
st.dataframe(
|
| 857 |
+
df_all,
|
| 858 |
+
width='stretch',
|
| 859 |
+
hide_index=True,
|
| 860 |
+
height=(num_rows * 36 + 36),
|
| 861 |
+
column_config={
|
| 862 |
+
"Resource": st.column_config.TextColumn("Resource", width="large"),
|
| 863 |
+
"Available Amount": st.column_config.TextColumn("Available Amount"),
|
| 864 |
+
"Type": st.column_config.TextColumn("Type"),
|
| 865 |
+
},
|
| 866 |
+
row_height=36
|
| 867 |
+
)
|
| 868 |
+
else:
|
| 869 |
st.error("No resources defined yet. Set availability above to populate the summary table.")
|
tabs/tab2_constraints.py
CHANGED
|
@@ -1,188 +1,201 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
st.session_state.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
#
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
)
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
st.warning("No active methods selected.")
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import streamlit as st
|
| 3 |
+
|
| 4 |
+
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips
|
| 5 |
+
from utils.ui_helpers import show_bar
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def default_constraint():
|
| 10 |
+
"""Return the default constraint dict for a CDR method (active, 100% cap).
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
dict: {"active": True, "cap_type": "percent", "cap_value": 100}.
|
| 14 |
+
"""
|
| 15 |
+
return {"active": True, "cap_type": "percent", "cap_value": int(100)}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def render_method(m, cols, i):
|
| 19 |
+
"""Render the constraint widget group (toggle + cap slider/input) for one method.
|
| 20 |
+
|
| 21 |
+
Writes the result into st.session_state.constraints[m].
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
m (str): CDR method name.
|
| 25 |
+
cols (list): list of Streamlit column objects.
|
| 26 |
+
i (int): index used to pick the column (i % 3).
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
None
|
| 30 |
+
"""
|
| 31 |
+
with cols[i % 3]:
|
| 32 |
+
tooltip = method_tooltips.get(m, None)
|
| 33 |
+
|
| 34 |
+
if m not in st.session_state.constraints:
|
| 35 |
+
st.session_state.constraints[m] = default_constraint()
|
| 36 |
+
|
| 37 |
+
constraint = st.session_state.constraints[m]
|
| 38 |
+
|
| 39 |
+
toggle_key = f"toggle_{m}"
|
| 40 |
+
cap_type_key = f"cap_type_{m}"
|
| 41 |
+
slider_key = f"slider_{m}"
|
| 42 |
+
manual_key = f"manual_{m}"
|
| 43 |
+
|
| 44 |
+
# init widgets ONLY from constraint (not reverse)
|
| 45 |
+
st.session_state.setdefault(toggle_key, constraint["active"])
|
| 46 |
+
st.session_state.setdefault(cap_type_key, constraint["cap_type"])
|
| 47 |
+
|
| 48 |
+
if constraint["cap_type"] == "percent":
|
| 49 |
+
st.session_state.setdefault(slider_key, int(constraint["cap_value"]))
|
| 50 |
+
else:
|
| 51 |
+
st.session_state.setdefault(manual_key, float(constraint["cap_value"]))
|
| 52 |
+
|
| 53 |
+
is_active = st.toggle(m, key=toggle_key, help=tooltip)
|
| 54 |
+
|
| 55 |
+
st.session_state.constraints[m]["active"] = is_active
|
| 56 |
+
|
| 57 |
+
if not is_active:
|
| 58 |
+
return
|
| 59 |
+
|
| 60 |
+
cap_type = st.radio(
|
| 61 |
+
"Cap value type",
|
| 62 |
+
["percent", "absolute"],
|
| 63 |
+
horizontal=True,
|
| 64 |
+
key=cap_type_key,
|
| 65 |
+
format_func=lambda x: "% of total potential"
|
| 66 |
+
if x == "percent"
|
| 67 |
+
else f"Absolute ({CO2_UNIT}/yr)",
|
| 68 |
+
label_visibility="collapsed",
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
st.session_state.constraints[m]["cap_type"] = cap_type
|
| 72 |
+
|
| 73 |
+
if cap_type == "percent":
|
| 74 |
+
|
| 75 |
+
value = st.slider(
|
| 76 |
+
"Max share (%)",
|
| 77 |
+
0, 100, step=5,
|
| 78 |
+
key=slider_key,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
st.session_state.constraints[m]["cap_value"] = value
|
| 82 |
+
|
| 83 |
+
if value == 0:
|
| 84 |
+
st.warning("Cap is 0% but method is active.", icon=":material/warning:")
|
| 85 |
+
|
| 86 |
+
else:
|
| 87 |
+
value = st.number_input(
|
| 88 |
+
f"Max cap ({CO2_UNIT}/yr)",
|
| 89 |
+
min_value=0.0,
|
| 90 |
+
format="%.2f",
|
| 91 |
+
value=None,
|
| 92 |
+
placeholder="0.00",
|
| 93 |
+
key=manual_key,
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
st.session_state.constraints[m]["cap_value"] = float(value) if value is not None else 0.0
|
| 97 |
+
|
| 98 |
+
if not value:
|
| 99 |
+
st.warning(f"Cap is 0 {CO2_UNIT}/yr but method is active.", icon=":material/warning:")
|
| 100 |
+
|
| 101 |
+
def reset_all_constraints():
|
| 102 |
+
"""Reset all method constraints to their default values and rerun the app.
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
None
|
| 106 |
+
"""
|
| 107 |
+
for m in method_order:
|
| 108 |
+
st.session_state.constraints[m] = default_constraint()
|
| 109 |
+
for key in [f"toggle_type_{m}", f"cap_type_{m}", f"slider_{m}_widget", f"manual_{m}_widget"]:
|
| 110 |
+
st.session_state.pop(key, None)
|
| 111 |
+
st.success("All method constraints reset to default.")
|
| 112 |
+
st.rerun()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def build_summary_df():
|
| 116 |
+
"""Build a summary DataFrame of currently active methods and their caps.
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
pd.DataFrame | None: index = method name, column "Cap value" as a formatted
|
| 120 |
+
string. Returns None if no method is active.
|
| 121 |
+
"""
|
| 122 |
+
rows = {}
|
| 123 |
+
for m in method_order:
|
| 124 |
+
c = st.session_state.constraints[m]
|
| 125 |
+
if not c["active"]:
|
| 126 |
+
continue
|
| 127 |
+
if c["cap_type"] == "percent":
|
| 128 |
+
rows[m] = {"Cap value": f"{c['cap_value']}%"}
|
| 129 |
+
else:
|
| 130 |
+
rows[m] = {"Cap value": f"{c['cap_value']:,.2f} {CO2_UNIT}/yr"}
|
| 131 |
+
return pd.DataFrame.from_dict(rows, orient="index") if rows else None
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def render_tab():
|
| 136 |
+
"""Render the Method Constraints tab.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
None
|
| 140 |
+
"""
|
| 141 |
+
st.markdown('<h1 class="crra-heading">Method constraints</h1>', unsafe_allow_html=True)
|
| 142 |
+
show_bar()
|
| 143 |
+
|
| 144 |
+
st.markdown(
|
| 145 |
+
"""
|
| 146 |
+
<p class="objective_title">OBJECTIVE</p>
|
| 147 |
+
""",
|
| 148 |
+
unsafe_allow_html=True,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
st.markdown(
|
| 152 |
+
"""
|
| 153 |
+
<div class="text_with_border">
|
| 154 |
+
<p>
|
| 155 |
+
This section lets you control which CDR methods are included in the optimization
|
| 156 |
+
and set a <strong>maximum deployment cap</strong> per method.<br>
|
| 157 |
+
<li>Activate or deactivate individual methods</li>
|
| 158 |
+
<li>Cap each method by <strong>% of its total potential</strong> or by an
|
| 159 |
+
<strong>absolute value</strong> in {CO2_UNIT}/yr</li>
|
| 160 |
+
</p>
|
| 161 |
+
</div>
|
| 162 |
+
""".format(CO2_UNIT=CO2_UNIT),
|
| 163 |
+
unsafe_allow_html=True,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
with st.expander("Manage methods", expanded=True, icon=":material/settings:"):
|
| 167 |
+
for group, method_list in method_groups.items():
|
| 168 |
+
st.markdown(f"""
|
| 169 |
+
<p class="crra-group-name">{group}</p>""",
|
| 170 |
+
unsafe_allow_html=True,
|
| 171 |
+
)
|
| 172 |
+
cols = st.columns(3)
|
| 173 |
+
for i, m in enumerate(method_list):
|
| 174 |
+
render_method(m, cols, i)
|
| 175 |
+
|
| 176 |
+
st.divider()
|
| 177 |
+
if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
|
| 178 |
+
reset_all_constraints()
|
| 179 |
+
|
| 180 |
+
st.markdown("""
|
| 181 |
+
<p class="crra-sub-heading">Summary of Active methods and caps</p>""",
|
| 182 |
+
unsafe_allow_html=True,
|
| 183 |
+
)
|
| 184 |
+
df = build_summary_df()
|
| 185 |
+
if df is not None:
|
| 186 |
+
df_display = df.reset_index().rename(columns={"index": "CDR Method"})
|
| 187 |
+
df_display = df_display[["CDR Method", "Cap value"]]
|
| 188 |
+
num_rows = len(df_display)
|
| 189 |
+
st.dataframe(
|
| 190 |
+
df_display,
|
| 191 |
+
width='stretch',
|
| 192 |
+
hide_index=True,
|
| 193 |
+
height=(num_rows * 36 + 36),
|
| 194 |
+
column_config={
|
| 195 |
+
"CDR Method": st.column_config.TextColumn("CDR Method", width="large"),
|
| 196 |
+
"Cap value": st.column_config.TextColumn("Cap value"),
|
| 197 |
+
},
|
| 198 |
+
row_height=36
|
| 199 |
+
)
|
| 200 |
+
else:
|
| 201 |
st.warning("No active methods selected.")
|
tabs/tab3_coefficients.py
CHANGED
|
@@ -1,422 +1,510 @@
|
|
| 1 |
-
import pandas as pd
|
| 2 |
-
import streamlit as st
|
| 3 |
-
|
| 4 |
-
from config.config import (method_groups, method_order, resource_groups,
|
| 5 |
-
resource_order, secondary_resources, resource_unit_map, CO2_UNIT)
|
| 6 |
-
from utils.ui_helpers import format_efficiency_summary, show_bar
|
| 7 |
-
from utils.data_utils import strip_unit_suffix
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
for r in resources
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
if
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
st.session_state.costs
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
)
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
)
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import streamlit as st
|
| 3 |
+
|
| 4 |
+
from config.config import (method_groups, method_order, resource_groups,
|
| 5 |
+
resource_order, secondary_resources, resource_unit_map, CO2_UNIT)
|
| 6 |
+
from utils.ui_helpers import format_efficiency_summary, show_bar
|
| 7 |
+
from utils.data_utils import strip_unit_suffix, get_resource_to_group
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def normalize_costs(costs_dict: dict) -> dict:
|
| 11 |
+
"""Strip unit suffixes from resource keys for legacy scenario compatibility."""
|
| 12 |
+
return {strip_unit_suffix(r): v for r, v in costs_dict.items()}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_slider_params(conf: dict) -> tuple:
|
| 16 |
+
"""Derive slider step size and display format from a coefficient config dict.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
conf (dict): slider config with key "min" (the minimum coefficient value).
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
tuple[float, str]: (step, fmt) where step is the slider increment and
|
| 23 |
+
fmt is a printf-style format string for display.
|
| 24 |
+
"""
|
| 25 |
+
min_coeff = conf["min"]
|
| 26 |
+
if min_coeff < 0.01:
|
| 27 |
+
return 0.0001, "%.4f"
|
| 28 |
+
elif min_coeff < 1:
|
| 29 |
+
return 0.001, "%.3f"
|
| 30 |
+
elif min_coeff < 100:
|
| 31 |
+
return 0.01, "%.2f"
|
| 32 |
+
else:
|
| 33 |
+
return 1.0, "%.0f"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def is_method_enabled(method, resource, enabled_methods):
|
| 37 |
+
"""Check whether a method is currently enabled for a given resource.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
method (str): CDR method name.
|
| 41 |
+
resource (str): resource name.
|
| 42 |
+
enabled_methods (dict): {resource: {method: bool}} from session state.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
bool: True if the method is enabled (defaults to True if not explicitly set).
|
| 46 |
+
"""
|
| 47 |
+
return enabled_methods.get(resource, {}).get(method, True)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def is_custom_secondary(method_name, custom_r, custom_resource_to_sub_group):
|
| 51 |
+
"""Return True if the reference resource for custom_r is secondary for method_name.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
method_name (str): CDR method name.
|
| 55 |
+
custom_r (str): custom batch resource name.
|
| 56 |
+
custom_resource_to_sub_group (dict): {custom_r: reference_resource}.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
bool
|
| 60 |
+
"""
|
| 61 |
+
sub_group = custom_resource_to_sub_group.get(custom_r)
|
| 62 |
+
return sub_group is not None and (method_name, sub_group) in secondary_resources
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def make_sync_s_to_m(slider_key, manual_key):
|
| 66 |
+
"""on_change for slider: mirror its value into the number_input."""
|
| 67 |
+
def sync():
|
| 68 |
+
st.session_state[manual_key] = st.session_state[slider_key]
|
| 69 |
+
return sync
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def make_sync_m_to_s(slider_key, manual_key, min_val, max_val):
|
| 73 |
+
"""on_change for number_input: clamp to [min, max] and sync both widgets."""
|
| 74 |
+
def sync():
|
| 75 |
+
val = st.session_state[manual_key]
|
| 76 |
+
if val is None:
|
| 77 |
+
return
|
| 78 |
+
clamped = max(min_val, min(max_val, val))
|
| 79 |
+
st.session_state[manual_key] = clamped
|
| 80 |
+
st.session_state[slider_key] = clamped
|
| 81 |
+
return sync
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def build_custom_slider_index(cost_sliders_norm, custom_resources):
|
| 85 |
+
"""Build the per-method slider configs and group mappings for custom resource batches.
|
| 86 |
+
|
| 87 |
+
Also initialises session-state cost entries for custom resources (primary at
|
| 88 |
+
median, secondary at 0) and removes stale entries for methods no longer linked
|
| 89 |
+
to a given batch.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
cost_sliders_norm (dict): {method: {resource: {min, median, max}}} with unit suffixes stripped.
|
| 93 |
+
custom_resources (list): list of custom batch dicts from session state.
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
tuple[dict, set, dict, dict]:
|
| 97 |
+
custom_sliders_by_method – {method: {resource: {min, median, max}}}.
|
| 98 |
+
custom_resource_names – set of all custom batch names.
|
| 99 |
+
custom_resource_to_sub_group – {custom_r: reference_resource}.
|
| 100 |
+
custom_resource_to_group – {custom_r: physical_group}.
|
| 101 |
+
"""
|
| 102 |
+
custom_sliders_by_method = {}
|
| 103 |
+
custom_resource_names = set()
|
| 104 |
+
for batch in custom_resources:
|
| 105 |
+
r_name = batch["name"]
|
| 106 |
+
custom_resource_names.add(r_name)
|
| 107 |
+
for m, coefs in batch["methods"].items():
|
| 108 |
+
custom_sliders_by_method.setdefault(m, {})[r_name] = coefs
|
| 109 |
+
ref = batch["group"]
|
| 110 |
+
for m, resources in cost_sliders_norm.items():
|
| 111 |
+
if (m, ref) in secondary_resources and r_name not in custom_sliders_by_method.get(m, {}):
|
| 112 |
+
ref_coefs = resources.get(ref)
|
| 113 |
+
if ref_coefs:
|
| 114 |
+
custom_sliders_by_method.setdefault(m, {})[r_name] = ref_coefs
|
| 115 |
+
|
| 116 |
+
custom_resource_to_sub_group = {batch["name"]: batch["group"] for batch in custom_resources}
|
| 117 |
+
resource_to_group = get_resource_to_group()
|
| 118 |
+
custom_resource_to_group = {
|
| 119 |
+
r: resource_to_group[s_g]
|
| 120 |
+
for r, s_g in custom_resource_to_sub_group.items()
|
| 121 |
+
if s_g in resource_to_group
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# Remove stale cost entries for methods no longer linked to a batch
|
| 125 |
+
valid_methods_for_custom = {}
|
| 126 |
+
for m, resources in custom_sliders_by_method.items():
|
| 127 |
+
for r in resources:
|
| 128 |
+
valid_methods_for_custom.setdefault(r, set()).add(m)
|
| 129 |
+
for r_name, valid in valid_methods_for_custom.items():
|
| 130 |
+
for m in list(st.session_state.get("costs", {}).keys()):
|
| 131 |
+
if r_name in st.session_state.costs.get(m, {}) and m not in valid:
|
| 132 |
+
del st.session_state.costs[m][r_name]
|
| 133 |
+
|
| 134 |
+
# Pre-initialise cost entries for custom resources
|
| 135 |
+
for m, resources in custom_sliders_by_method.items():
|
| 136 |
+
st.session_state.costs.setdefault(m, {})
|
| 137 |
+
for r, conf in resources.items():
|
| 138 |
+
if r not in st.session_state.costs[m]:
|
| 139 |
+
st.session_state.costs[m][r] = (
|
| 140 |
+
0.0 if is_custom_secondary(m, r, custom_resource_to_sub_group) else conf["median"]
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
return custom_sliders_by_method, custom_resource_names, custom_resource_to_sub_group, custom_resource_to_group
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def render_standard_resource_row(method, r, conf, display_label, enabled_methods):
|
| 147 |
+
"""Render the slider row for a single standard resource coefficient.
|
| 148 |
+
|
| 149 |
+
Handles the secondary-resource toggle, fixed-value display, and the
|
| 150 |
+
slider + manual number_input pair. Writes the result to
|
| 151 |
+
st.session_state.costs[method][r].
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
method (str): CDR method name.
|
| 155 |
+
r (str): resource name.
|
| 156 |
+
conf (dict): {min, median, max} for this (method, resource) pair.
|
| 157 |
+
display_label (str): human-readable resource label with unit.
|
| 158 |
+
enabled_methods (dict): {resource: {method: bool}} from session state.
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
None
|
| 162 |
+
"""
|
| 163 |
+
if not is_method_enabled(method, r, enabled_methods):
|
| 164 |
+
st.info(f"INFO: Method disabled for **{r}** (see Resource Inputs tab)")
|
| 165 |
+
return
|
| 166 |
+
|
| 167 |
+
is_secondary = (method, r) in secondary_resources
|
| 168 |
+
step, fmt = get_slider_params(conf)
|
| 169 |
+
toggle_key = f"toggle_{method}_{r}".replace(" ", "_").lower()
|
| 170 |
+
slider_key = f"slider_{method}_{r}".replace(" ", "_").lower()
|
| 171 |
+
manual_key = f"manual_{method}_{r}".replace(" ", "_").lower()
|
| 172 |
+
current_val = normalize_costs(st.session_state.costs.get(method, {})).get(r, 0.0)
|
| 173 |
+
|
| 174 |
+
if is_secondary:
|
| 175 |
+
if toggle_key not in st.session_state:
|
| 176 |
+
st.session_state[toggle_key] = current_val != 0.0
|
| 177 |
+
if not st.checkbox(f"Use {display_label}", key=toggle_key):
|
| 178 |
+
st.session_state.costs[method][r] = 0.0
|
| 179 |
+
st.markdown("**DISABLED**")
|
| 180 |
+
return
|
| 181 |
+
elif current_val == 0.0:
|
| 182 |
+
st.session_state.costs[method][r] = conf["median"]
|
| 183 |
+
current_val = conf["median"]
|
| 184 |
+
|
| 185 |
+
if conf["min"] == conf["max"]:
|
| 186 |
+
st.markdown(
|
| 187 |
+
f'<p class="crra-resource-name">{display_label} = <strong>{conf["median"]}</strong> (fixed)</p>',
|
| 188 |
+
unsafe_allow_html=True,
|
| 189 |
+
)
|
| 190 |
+
st.session_state.costs[method][r] = conf["median"]
|
| 191 |
+
if st.session_state.get("resource_caps", {}).get(r, 0) <= 0:
|
| 192 |
+
st.info(
|
| 193 |
+
f"No available quantity set for **{r}** in Resource Inputs tab: "
|
| 194 |
+
f"**{method}** won't be used in the optimization.",
|
| 195 |
+
icon=":material/info:",
|
| 196 |
+
)
|
| 197 |
+
return
|
| 198 |
+
|
| 199 |
+
if slider_key not in st.session_state:
|
| 200 |
+
st.session_state[slider_key] = current_val
|
| 201 |
+
if manual_key not in st.session_state:
|
| 202 |
+
st.session_state[manual_key] = st.session_state[slider_key]
|
| 203 |
+
if st.session_state.get("resource_caps", {}).get(r, 0) <= 0:
|
| 204 |
+
st.info(
|
| 205 |
+
f"No available quantity set for **{r}** in Resource Inputs tab: "
|
| 206 |
+
f"**{method}** won't be used in the optimization.",
|
| 207 |
+
icon=":material/info:",
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
col1, col2 = st.columns([3, 1])
|
| 211 |
+
with col1:
|
| 212 |
+
slider_val = st.slider(
|
| 213 |
+
label=display_label,
|
| 214 |
+
min_value=conf["min"], max_value=conf["max"], step=step, format=fmt,
|
| 215 |
+
key=slider_key,
|
| 216 |
+
on_change=make_sync_s_to_m(slider_key, manual_key),
|
| 217 |
+
)
|
| 218 |
+
with col2:
|
| 219 |
+
st.number_input(
|
| 220 |
+
label="label", label_visibility="collapsed",
|
| 221 |
+
key=manual_key, help="Manual override (optional, any value)",
|
| 222 |
+
format=fmt, value=None, placeholder=str(round(slider_val, 4)),
|
| 223 |
+
on_change=make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]),
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
manual_value = st.session_state.get(manual_key)
|
| 227 |
+
st.session_state.costs[method][r] = slider_val if manual_value is None else manual_value
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def render_custom_resource_row(method, r, conf, enabled_methods, custom_resource_to_sub_group, custom_resources):
|
| 231 |
+
"""Render the slider row for a single custom resource batch coefficient.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
method (str): CDR method name.
|
| 235 |
+
r (str): custom resource batch name.
|
| 236 |
+
conf (dict): {min, median, max} for this (method, resource) pair.
|
| 237 |
+
enabled_methods (dict): {resource: {method: bool}} from session state.
|
| 238 |
+
custom_resource_to_sub_group (dict): {custom_r: reference_resource}.
|
| 239 |
+
custom_resources (list): list of custom batch dicts from session state.
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
None
|
| 243 |
+
"""
|
| 244 |
+
if not is_method_enabled(method, r, enabled_methods):
|
| 245 |
+
st.info("INFO: Method disabled for this resource (see Resource Inputs tab)")
|
| 246 |
+
return
|
| 247 |
+
|
| 248 |
+
is_sec = is_custom_secondary(method, r, custom_resource_to_sub_group)
|
| 249 |
+
step, fmt = get_slider_params(conf)
|
| 250 |
+
toggle_key = f"toggle_{method}_{r}".replace(" ", "_").lower()
|
| 251 |
+
slider_key = f"slider_{method}_{r}".replace(" ", "_").lower()
|
| 252 |
+
manual_key = f"manual_{method}_{r}".replace(" ", "_").lower()
|
| 253 |
+
current_val = st.session_state.costs.get(method, {}).get(r, 0.0 if is_sec else conf["median"])
|
| 254 |
+
|
| 255 |
+
if is_sec:
|
| 256 |
+
if toggle_key not in st.session_state:
|
| 257 |
+
st.session_state[toggle_key] = current_val != 0.0
|
| 258 |
+
if not st.checkbox(f"Use {r}", key=toggle_key):
|
| 259 |
+
st.session_state.costs.setdefault(method, {})[r] = 0.0
|
| 260 |
+
st.markdown("**DISABLED**")
|
| 261 |
+
return
|
| 262 |
+
elif current_val == 0.0:
|
| 263 |
+
st.session_state.costs.setdefault(method, {})[r] = conf["median"]
|
| 264 |
+
current_val = conf["median"]
|
| 265 |
+
|
| 266 |
+
if slider_key not in st.session_state:
|
| 267 |
+
st.session_state[slider_key] = current_val
|
| 268 |
+
if manual_key not in st.session_state:
|
| 269 |
+
st.session_state[manual_key] = st.session_state[slider_key]
|
| 270 |
+
|
| 271 |
+
batch_amount = next((float(b.get("amount", 0)) for b in custom_resources if b["name"] == r), 0.0)
|
| 272 |
+
if batch_amount <= 0:
|
| 273 |
+
st.info(
|
| 274 |
+
f"No available quantity set for **{r}** in Resource Inputs — "
|
| 275 |
+
"this method won't be used in the optimization.",
|
| 276 |
+
icon=":material/info:",
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
col1, col2 = st.columns([3, 1])
|
| 280 |
+
with col1:
|
| 281 |
+
slider_val = st.slider(
|
| 282 |
+
label=r,
|
| 283 |
+
min_value=conf["min"], max_value=conf["max"], step=step, format=fmt,
|
| 284 |
+
key=slider_key,
|
| 285 |
+
on_change=make_sync_s_to_m(slider_key, manual_key),
|
| 286 |
+
)
|
| 287 |
+
with col2:
|
| 288 |
+
st.number_input(
|
| 289 |
+
label="label", label_visibility="collapsed",
|
| 290 |
+
key=manual_key, help="Manual override (optional, any value)",
|
| 291 |
+
format=fmt, value=None, placeholder=str(round(slider_val, 4)),
|
| 292 |
+
on_change=make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]),
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
st.session_state.costs.setdefault(method, {})
|
| 296 |
+
manual_value = st.session_state.get(manual_key)
|
| 297 |
+
st.session_state.costs[method][r] = slider_val if manual_value is None else manual_value
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def panel_method_editor(
|
| 301 |
+
method, cost_sliders_norm, custom_sliders_by_method,
|
| 302 |
+
resource_display, enabled_methods, custom_resources,
|
| 303 |
+
custom_resource_to_group, custom_resource_to_sub_group,
|
| 304 |
+
):
|
| 305 |
+
"""Render the coefficient-editing expander for one CDR method.
|
| 306 |
+
|
| 307 |
+
Shows one slider row per standard resource, then one per custom resource batch,
|
| 308 |
+
grouped by resource group. Includes a per-method reset button.
|
| 309 |
+
|
| 310 |
+
Args:
|
| 311 |
+
method (str): CDR method name.
|
| 312 |
+
cost_sliders_norm (dict): {method: {resource: {min, median, max}}}.
|
| 313 |
+
custom_sliders_by_method (dict): {method: {resource: {min, median, max}}}.
|
| 314 |
+
resource_display (dict): {resource: label_with_unit}.
|
| 315 |
+
enabled_methods (dict): {resource: {method: bool}} from session state.
|
| 316 |
+
custom_resources (list): list of custom batch dicts from session state.
|
| 317 |
+
custom_resource_to_group (dict): {custom_r: physical_group}.
|
| 318 |
+
custom_resource_to_sub_group (dict): {custom_r: reference_resource}.
|
| 319 |
+
|
| 320 |
+
Returns:
|
| 321 |
+
None
|
| 322 |
+
"""
|
| 323 |
+
with st.expander(f"Manage resources for **{method}**", expanded=True):
|
| 324 |
+
method_sliders = cost_sliders_norm.get(method, {})
|
| 325 |
+
custom_for_method = custom_sliders_by_method.get(method, {})
|
| 326 |
+
|
| 327 |
+
for group_name, rlist in resource_groups.items():
|
| 328 |
+
used = [r for r in rlist if r in method_sliders]
|
| 329 |
+
custom_in_group = [
|
| 330 |
+
r for r in custom_for_method
|
| 331 |
+
if custom_resource_to_group.get(r) == group_name
|
| 332 |
+
]
|
| 333 |
+
if not used and not custom_in_group:
|
| 334 |
+
continue
|
| 335 |
+
|
| 336 |
+
st.markdown(f'<p class="crra-group-name">{group_name}</p>', unsafe_allow_html=True)
|
| 337 |
+
|
| 338 |
+
for r in used:
|
| 339 |
+
conf = method_sliders[r]
|
| 340 |
+
render_standard_resource_row(
|
| 341 |
+
method, r, conf, resource_display.get(r, r), enabled_methods
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
for r in custom_in_group:
|
| 345 |
+
render_custom_resource_row(
|
| 346 |
+
method, r, custom_for_method[r],
|
| 347 |
+
enabled_methods, custom_resource_to_sub_group, custom_resources
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
if st.button("Reset selected method", key="btn-reset-method", icon=":material/reset_settings:"):
|
| 351 |
+
for r, conf in cost_sliders_norm.get(method, {}).items():
|
| 352 |
+
is_secondary = (method, r) in secondary_resources
|
| 353 |
+
st.session_state.costs[method][r] = 0.0 if is_secondary else conf["median"]
|
| 354 |
+
for prefix in ["slider", "manual", "toggle"]:
|
| 355 |
+
st.session_state.pop(f"{prefix}_{method}_{r}".replace(" ", "_").lower(), None)
|
| 356 |
+
for r, conf in custom_sliders_by_method.get(method, {}).items():
|
| 357 |
+
st.session_state.costs.setdefault(method, {})[r] = conf["median"]
|
| 358 |
+
for prefix in ["slider", "manual"]:
|
| 359 |
+
st.session_state.pop(f"{prefix}_{method}_{r}".replace(" ", "_").lower(), None)
|
| 360 |
+
st.success(f"{method} reset to default values.")
|
| 361 |
+
st.rerun()
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def panel_summary_table(
|
| 365 |
+
cost_sliders_norm, custom_sliders_by_method,
|
| 366 |
+
enabled_methods, custom_resource_names, custom_resource_to_sub_group,
|
| 367 |
+
):
|
| 368 |
+
"""Render the full coefficient summary table with cells highlighted when modified.
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
cost_sliders_norm (dict): {method: {resource: {min, median, max}}}.
|
| 372 |
+
custom_sliders_by_method (dict): {method: {resource: {min, median, max}}}.
|
| 373 |
+
enabled_methods (dict): {resource: {method: bool}} from session state.
|
| 374 |
+
custom_resource_names (set[str]): names of all custom resource batches.
|
| 375 |
+
custom_resource_to_sub_group (dict): {custom_r: reference_resource}.
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
None
|
| 379 |
+
"""
|
| 380 |
+
st.markdown(
|
| 381 |
+
'<p class="crra-sub-heading">Summary of resource usage coefficients</p>',
|
| 382 |
+
unsafe_allow_html=True,
|
| 383 |
+
)
|
| 384 |
+
resource_display = {
|
| 385 |
+
r: f"{r} ({resource_unit_map[cat]}/{CO2_UNIT})"
|
| 386 |
+
for cat, resources in resource_groups.items()
|
| 387 |
+
for r in resources
|
| 388 |
+
}
|
| 389 |
+
all_resource_columns = resource_order + sorted(custom_resource_names)
|
| 390 |
+
|
| 391 |
+
df_default = pd.DataFrame(index=method_order, columns=all_resource_columns)
|
| 392 |
+
df_current = pd.DataFrame(index=method_order, columns=all_resource_columns)
|
| 393 |
+
|
| 394 |
+
for m in method_order:
|
| 395 |
+
current_costs = normalize_costs(st.session_state.costs.get(m, {}))
|
| 396 |
+
sliders = dict(cost_sliders_norm.get(m, {}))
|
| 397 |
+
sliders.update(custom_sliders_by_method.get(m, {}))
|
| 398 |
+
|
| 399 |
+
for r in all_resource_columns:
|
| 400 |
+
is_sec = (m, r) in secondary_resources or is_custom_secondary(m, r, custom_resource_to_sub_group)
|
| 401 |
+
disabled = not is_method_enabled(m, r, enabled_methods)
|
| 402 |
+
current_val = 0.0 if disabled else current_costs.get(r, 0.0)
|
| 403 |
+
default_val = 0.0 if disabled else sliders.get(r, {}).get("median", 0.0)
|
| 404 |
+
if is_sec and current_val == 0.0:
|
| 405 |
+
default_val = 0.0
|
| 406 |
+
df_default.at[m, r] = default_val
|
| 407 |
+
df_current.at[m, r] = current_val
|
| 408 |
+
|
| 409 |
+
df_default = df_default.astype(float).fillna(0.0)
|
| 410 |
+
df_current = df_current.astype(float).fillna(0.0)
|
| 411 |
+
|
| 412 |
+
resource_display_full = dict(resource_display)
|
| 413 |
+
resource_display_full.update({r: r for r in custom_resource_names})
|
| 414 |
+
df_default = df_default.rename(columns=resource_display_full)
|
| 415 |
+
df_current = df_current.rename(columns=resource_display_full)
|
| 416 |
+
|
| 417 |
+
styled = format_efficiency_summary(df_current, df_default)
|
| 418 |
+
st.dataframe(
|
| 419 |
+
data=styled,
|
| 420 |
+
height=(len(df_current) * 36 + 36),
|
| 421 |
+
width='stretch',
|
| 422 |
+
column_config={"_index": st.column_config.TextColumn("Method", width="large")},
|
| 423 |
+
row_height=36
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def render_tab(cost_sliders):
|
| 428 |
+
"""Render the Resource Efficiency Coefficients tab.
|
| 429 |
+
|
| 430 |
+
Args:
|
| 431 |
+
cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}.
|
| 432 |
+
|
| 433 |
+
Returns:
|
| 434 |
+
None
|
| 435 |
+
"""
|
| 436 |
+
st.markdown('<h1 class="crra-heading">Resource efficiency coefficients</h1>', unsafe_allow_html=True)
|
| 437 |
+
show_bar()
|
| 438 |
+
|
| 439 |
+
st.markdown('<p class="objective_title">OBJECTIVE</p>', unsafe_allow_html=True)
|
| 440 |
+
st.markdown(
|
| 441 |
+
'<div class="text_with_border"><p>'
|
| 442 |
+
"This section lets you adjust the resource usage coefficients for each CDR method "
|
| 443 |
+
"to best fit your country conditions.<br>"
|
| 444 |
+
"<li>Primary resources are always active and initialized with default values.</li>"
|
| 445 |
+
"<li>Secondary resources are initialized at zero and toggled off by default.</li>"
|
| 446 |
+
"<li>Toggling on a secondary resource restores its default value.</li>"
|
| 447 |
+
"</p></div>",
|
| 448 |
+
unsafe_allow_html=True,
|
| 449 |
+
)
|
| 450 |
+
with st.container(key="warning_resources"):
|
| 451 |
+
st.warning(
|
| 452 |
+
"Red cells in the table highlight coefficients modified from their **default median values**",
|
| 453 |
+
icon=":material/warning:",
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 457 |
+
custom_resources = st.session_state.get("custom_resources", [])
|
| 458 |
+
|
| 459 |
+
cost_sliders_norm = {m: normalize_costs(resources) for m, resources in cost_sliders.items()}
|
| 460 |
+
resource_display = {
|
| 461 |
+
r: f"{r} ({resource_unit_map[cat]}/{CO2_UNIT})"
|
| 462 |
+
for cat, resources in resource_groups.items()
|
| 463 |
+
for r in resources
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
(
|
| 467 |
+
custom_sliders_by_method,
|
| 468 |
+
custom_resource_names,
|
| 469 |
+
custom_resource_to_sub_group,
|
| 470 |
+
custom_resource_to_group,
|
| 471 |
+
) = build_custom_slider_index(cost_sliders_norm, custom_resources)
|
| 472 |
+
|
| 473 |
+
method_choices = [f"{g} | {m}" for g, methods in method_groups.items() for m in methods]
|
| 474 |
+
method_lookup = {label: label.split(" | ", 1)[1] for label in method_choices}
|
| 475 |
+
selected_label = st.selectbox("Select a method:", method_choices, key="crra-select")
|
| 476 |
+
|
| 477 |
+
if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
|
| 478 |
+
for m, sliders in cost_sliders_norm.items():
|
| 479 |
+
for r, conf in sliders.items():
|
| 480 |
+
st.session_state.costs[m][r] = 0.0 if (m, r) in secondary_resources else conf["median"]
|
| 481 |
+
for prefix in ["slider", "manual", "toggle"]:
|
| 482 |
+
st.session_state.pop(f"{prefix}_{m}_{r}".replace(" ", "_").lower(), None)
|
| 483 |
+
for m, sliders in custom_sliders_by_method.items():
|
| 484 |
+
for r, conf in sliders.items():
|
| 485 |
+
st.session_state.costs.setdefault(m, {})[r] = (
|
| 486 |
+
0.0 if is_custom_secondary(m, r, custom_resource_to_sub_group) else conf["median"]
|
| 487 |
+
)
|
| 488 |
+
for prefix in ["slider", "manual", "toggle"]:
|
| 489 |
+
st.session_state.pop(f"{prefix}_{m}_{r}".replace(" ", "_").lower(), None)
|
| 490 |
+
st.success("All methods reset to default values.")
|
| 491 |
+
st.rerun()
|
| 492 |
+
|
| 493 |
+
panel_method_editor(
|
| 494 |
+
method=method_lookup[selected_label],
|
| 495 |
+
cost_sliders_norm=cost_sliders_norm,
|
| 496 |
+
custom_sliders_by_method=custom_sliders_by_method,
|
| 497 |
+
resource_display=resource_display,
|
| 498 |
+
enabled_methods=enabled_methods,
|
| 499 |
+
custom_resources=custom_resources,
|
| 500 |
+
custom_resource_to_group=custom_resource_to_group,
|
| 501 |
+
custom_resource_to_sub_group=custom_resource_to_sub_group,
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
panel_summary_table(
|
| 505 |
+
cost_sliders_norm=cost_sliders_norm,
|
| 506 |
+
custom_sliders_by_method=custom_sliders_by_method,
|
| 507 |
+
enabled_methods=enabled_methods,
|
| 508 |
+
custom_resource_names=custom_resource_names,
|
| 509 |
+
custom_resource_to_sub_group=custom_resource_to_sub_group,
|
| 510 |
)
|
tabs/tab4_optimization.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tabs/tab5_sensitivity.py
CHANGED
|
@@ -1,185 +1,245 @@
|
|
| 1 |
-
import copy
|
| 2 |
-
import pandas as pd
|
| 3 |
-
import streamlit as st
|
| 4 |
-
import plotly.express as px
|
| 5 |
-
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
from utils.ui_helpers import register_chart_data, show_bar
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from config.config import CO2_UNIT, method_order, resource_order
|
| 6 |
+
from optimization.optimization import run_optimization
|
| 7 |
+
from utils.data_utils import get_resource_to_unit
|
| 8 |
+
from utils.ui_helpers import register_chart_data, show_bar
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def compute_baseline():
|
| 12 |
+
"""Run the baseline optimisation and build combined resource metadata.
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
tuple[float, list[str], dict[str, str]]:
|
| 16 |
+
baseline_removed – total CO₂ removed at current inputs (0 if failed).
|
| 17 |
+
all_resources – standard resources + custom batch names.
|
| 18 |
+
resource_to_unit – {resource: unit} including custom batches.
|
| 19 |
+
"""
|
| 20 |
+
baseline_success, baseline_result = run_optimization(
|
| 21 |
+
resource_caps=st.session_state.resource_caps,
|
| 22 |
+
method_constraints=st.session_state.constraints,
|
| 23 |
+
method_costs=st.session_state.costs,
|
| 24 |
+
custom_resources=st.session_state.get("custom_resources", []),
|
| 25 |
+
)
|
| 26 |
+
baseline_removed = baseline_result["total_removed"] if baseline_success else 0
|
| 27 |
+
|
| 28 |
+
custom_resource_names = [b["name"] for b in st.session_state.get("custom_resources", [])]
|
| 29 |
+
all_resources = resource_order + custom_resource_names
|
| 30 |
+
|
| 31 |
+
resource_to_unit = get_resource_to_unit()
|
| 32 |
+
for b in st.session_state.get("custom_resources", []):
|
| 33 |
+
resource_to_unit[b["name"]] = b.get("unit", "")
|
| 34 |
+
|
| 35 |
+
return baseline_removed, all_resources, resource_to_unit
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def panel_resource_sensitivity(baseline_removed, all_resources, resource_to_unit):
|
| 39 |
+
"""Render the resource-availability sensitivity sub-panel.
|
| 40 |
+
|
| 41 |
+
Sweeps each selected resource from 0 % to 200 % of its current cap in
|
| 42 |
+
10 % steps, re-solves the LP at each point, and plots total CO₂ removed.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
baseline_removed (float): total CO₂ removed at current inputs.
|
| 46 |
+
all_resources (list[str]): standard + custom resource names to select from.
|
| 47 |
+
resource_to_unit (dict[str, str]): {resource: unit} for axis labels.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
None
|
| 51 |
+
"""
|
| 52 |
+
st.markdown(
|
| 53 |
+
'<p class="crra-sub-heading">Impact of resource availability</p>',
|
| 54 |
+
unsafe_allow_html=True,
|
| 55 |
+
)
|
| 56 |
+
selected_resources = st.multiselect(
|
| 57 |
+
"Select one or more resources:", all_resources, key="crra-select-resource"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if not selected_resources:
|
| 61 |
+
st.warning("Please select at least one resource to run the sensitivity analysis.")
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
percentages = list(range(0, 210, 10))
|
| 65 |
+
custom_amount_by_name = {
|
| 66 |
+
b["name"]: float(b.get("amount", 0))
|
| 67 |
+
for b in st.session_state.get("custom_resources", [])
|
| 68 |
+
}
|
| 69 |
+
all_data = []
|
| 70 |
+
|
| 71 |
+
for resource in selected_resources:
|
| 72 |
+
unit = resource_to_unit.get(resource, "")
|
| 73 |
+
base_cap = (
|
| 74 |
+
st.session_state.resource_caps.get(resource)
|
| 75 |
+
or custom_amount_by_name.get(resource, 0)
|
| 76 |
+
)
|
| 77 |
+
for pct in percentages:
|
| 78 |
+
perturbed_caps = st.session_state.resource_caps.copy()
|
| 79 |
+
actual_amount = base_cap * pct / 100
|
| 80 |
+
perturbed_caps[resource] = actual_amount
|
| 81 |
+
|
| 82 |
+
success, result = run_optimization(
|
| 83 |
+
resource_caps=perturbed_caps,
|
| 84 |
+
method_constraints=st.session_state.constraints,
|
| 85 |
+
method_costs=st.session_state.costs,
|
| 86 |
+
custom_resources=st.session_state.get("custom_resources", []),
|
| 87 |
+
)
|
| 88 |
+
all_data.append({
|
| 89 |
+
"Input": resource,
|
| 90 |
+
"% of cap": pct,
|
| 91 |
+
"Actual amount": round(actual_amount, 4),
|
| 92 |
+
"Unit": unit,
|
| 93 |
+
f"CO₂ removed ({CO2_UNIT})": result["total_removed"] if success else 0,
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
df = pd.DataFrame(all_data)
|
| 97 |
+
st.markdown("#### Sensitivity to resource availability")
|
| 98 |
+
fig = px.line(
|
| 99 |
+
df, x="% of cap", y=f"CO₂ removed ({CO2_UNIT})", color="Input", markers=True,
|
| 100 |
+
hover_data={"Actual amount": ":.4f", "Unit": True},
|
| 101 |
+
)
|
| 102 |
+
fig.update_layout(
|
| 103 |
+
xaxis_title="Resource availability (% of current cap)",
|
| 104 |
+
yaxis_title=f"Total portfolio CO₂ removal ({CO2_UNIT})",
|
| 105 |
+
)
|
| 106 |
+
fig.add_hline(
|
| 107 |
+
y=baseline_removed, line_dash="dash", line_color="gray",
|
| 108 |
+
annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}",
|
| 109 |
+
)
|
| 110 |
+
st.plotly_chart(fig, width='stretch')
|
| 111 |
+
with st.expander("📊 View / download chart data"):
|
| 112 |
+
st.dataframe(df, width='stretch', hide_index=True)
|
| 113 |
+
register_chart_data("Sensitivity to resource availability", df)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def panel_method_sensitivity(baseline_removed):
|
| 117 |
+
"""Render the method-cap sensitivity sub-panel.
|
| 118 |
+
|
| 119 |
+
Sweeps each selected method's cap from 0 % to 100 % of its current value
|
| 120 |
+
in 10 % steps, re-solves the LP at each point, and plots total CO₂ removed.
|
| 121 |
+
Skips methods whose current cap is 0.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
baseline_removed (float): total CO₂ removed at current inputs.
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
None
|
| 128 |
+
"""
|
| 129 |
+
st.markdown(
|
| 130 |
+
'<p class="crra-sub-heading">Impact of max allocation per method</p>',
|
| 131 |
+
unsafe_allow_html=True,
|
| 132 |
+
)
|
| 133 |
+
selected_methods = st.multiselect(
|
| 134 |
+
"Select one or more methods:", method_order, key="crra-select-method"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
if not selected_methods:
|
| 138 |
+
st.warning("Please select at least one method to run the sensitivity analysis.")
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
percentages = list(range(0, 110, 10))
|
| 142 |
+
all_data = []
|
| 143 |
+
skipped = []
|
| 144 |
+
|
| 145 |
+
for method in selected_methods:
|
| 146 |
+
cap_type = st.session_state.constraints[method].get("cap_type", "percent")
|
| 147 |
+
current_cap = st.session_state.constraints[method].get(
|
| 148 |
+
"cap_value", 100 if cap_type == "percent" else 0.0
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
if current_cap <= 0:
|
| 152 |
+
skipped.append(method)
|
| 153 |
+
continue
|
| 154 |
+
|
| 155 |
+
for pct in percentages:
|
| 156 |
+
perturbed_constraints = copy.deepcopy(st.session_state.constraints)
|
| 157 |
+
if cap_type == "percent":
|
| 158 |
+
actual_cap = min(100, current_cap * pct / 100)
|
| 159 |
+
perturbed_constraints[method]["cap_value"] = actual_cap
|
| 160 |
+
cap_label = f"{actual_cap:.0f}% of potential"
|
| 161 |
+
else:
|
| 162 |
+
actual_cap = current_cap * pct / 100
|
| 163 |
+
perturbed_constraints[method]["cap_value"] = actual_cap
|
| 164 |
+
cap_label = f"{actual_cap:.2f} {CO2_UNIT}/yr"
|
| 165 |
+
|
| 166 |
+
success, result = run_optimization(
|
| 167 |
+
resource_caps=st.session_state.resource_caps,
|
| 168 |
+
method_constraints=perturbed_constraints,
|
| 169 |
+
method_costs=st.session_state.costs,
|
| 170 |
+
custom_resources=st.session_state.get("custom_resources", []),
|
| 171 |
+
)
|
| 172 |
+
all_data.append({
|
| 173 |
+
"Method": method,
|
| 174 |
+
"Cap type": cap_type,
|
| 175 |
+
"% of current cap": pct,
|
| 176 |
+
"Actual cap": cap_label,
|
| 177 |
+
f"CO₂ removed ({CO2_UNIT})": result["total_removed"] if success else 0,
|
| 178 |
+
})
|
| 179 |
+
|
| 180 |
+
for m in skipped:
|
| 181 |
+
st.warning(f"Method **{m}** has a cap of 0 — set a value first to include it.", icon="⚠️")
|
| 182 |
+
|
| 183 |
+
if not all_data:
|
| 184 |
+
return
|
| 185 |
+
|
| 186 |
+
df = pd.DataFrame(all_data)
|
| 187 |
+
fig = px.line(
|
| 188 |
+
df, x="% of current cap", y=f"CO₂ removed ({CO2_UNIT})",
|
| 189 |
+
color="Method", markers=True,
|
| 190 |
+
hover_data={"Actual cap": True, "Cap type": True},
|
| 191 |
+
)
|
| 192 |
+
fig.add_hline(
|
| 193 |
+
y=baseline_removed, line_dash="dash", line_color="lightgray",
|
| 194 |
+
annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}",
|
| 195 |
+
)
|
| 196 |
+
fig.update_layout(
|
| 197 |
+
xaxis_title="% of current cap setting",
|
| 198 |
+
yaxis_title=f"Total portfolio CO₂ removal ({CO2_UNIT})",
|
| 199 |
+
)
|
| 200 |
+
st.plotly_chart(fig, width='stretch')
|
| 201 |
+
with st.expander("📊 View / download chart data"):
|
| 202 |
+
st.dataframe(df, width='stretch', hide_index=True)
|
| 203 |
+
register_chart_data("Sensitivity to method cap", df)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def render_tab():
|
| 207 |
+
"""Render the Sensitivity Analysis tab.
|
| 208 |
+
|
| 209 |
+
Computes a baseline optimisation from session state, then lets the user
|
| 210 |
+
sweep one or more resources (or method caps) over a range and plots how
|
| 211 |
+
total CO₂ removed changes. Requires resource_caps, constraints, and costs
|
| 212 |
+
to be present in session state.
|
| 213 |
+
|
| 214 |
+
Returns:
|
| 215 |
+
None
|
| 216 |
+
"""
|
| 217 |
+
st.markdown('<h1 class="crra-heading">Sensitivity analysis</h1>', unsafe_allow_html=True)
|
| 218 |
+
show_bar()
|
| 219 |
+
|
| 220 |
+
st.markdown('<p class="objective_title">OBJECTIVE</p>', unsafe_allow_html=True)
|
| 221 |
+
st.markdown(
|
| 222 |
+
'<div class="text_with_border">'
|
| 223 |
+
"<p>Explore how varying <strong>one or multiple resources' availability</strong> "
|
| 224 |
+
"or <strong>methods' maximum allocation</strong> affects total CO₂ removed.</p>"
|
| 225 |
+
"</div>",
|
| 226 |
+
unsafe_allow_html=True,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
if (
|
| 230 |
+
"resource_caps" not in st.session_state
|
| 231 |
+
or "constraints" not in st.session_state
|
| 232 |
+
or "costs" not in st.session_state
|
| 233 |
+
):
|
| 234 |
+
st.warning("Please complete the setup in previous tabs before running sensitivity analysis.")
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
baseline_removed, all_resources, resource_to_unit = compute_baseline()
|
| 238 |
+
|
| 239 |
+
tab1, tab2 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity"])
|
| 240 |
+
|
| 241 |
+
with tab1:
|
| 242 |
+
panel_resource_sensitivity(baseline_removed, all_resources, resource_to_unit)
|
| 243 |
+
|
| 244 |
+
with tab2:
|
| 245 |
+
panel_method_sensitivity(baseline_removed)
|
tabs/tab6_scenarios.py
CHANGED
|
@@ -1,168 +1,199 @@
|
|
| 1 |
-
import json
|
| 2 |
-
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
st.markdown(
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
row[f"{
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
st.
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
_show_resource_usage(df_resources)
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
import streamlit as st
|
| 5 |
+
from config.config import CO2_UNIT
|
| 6 |
+
from utils.data_utils import is_valid_scenario
|
| 7 |
+
from tabs.tab4_optimization import parse_zip_scenario
|
| 8 |
+
from utils.ui_helpers import show_bar
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def panel_load_scenario():
|
| 12 |
+
"""Render the file uploader panel to load one or more scenarios for comparison.
|
| 13 |
+
|
| 14 |
+
Accepts JSON and ZIP files. Adds valid scenarios to st.session_state.scenarios.
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
None
|
| 18 |
+
"""
|
| 19 |
+
st.markdown("""
|
| 20 |
+
<p class="crra-sub-heading">Load a scenario from file</p>""",
|
| 21 |
+
unsafe_allow_html=True,
|
| 22 |
+
)
|
| 23 |
+
st.markdown('<p class="caption">Accepts `.json` or `.zip` (CSV export)</p>', unsafe_allow_html=True)
|
| 24 |
+
|
| 25 |
+
uploaded = st.file_uploader("Upload scenario", type=["json", "zip"], key="comparison_loader", accept_multiple_files=True)
|
| 26 |
+
if uploaded is None:
|
| 27 |
+
return
|
| 28 |
+
for uploaded_file in uploaded:
|
| 29 |
+
if uploaded_file is not None:
|
| 30 |
+
try:
|
| 31 |
+
if uploaded_file.name.endswith(".json"):
|
| 32 |
+
data = json.load(uploaded_file)
|
| 33 |
+
if not is_valid_scenario(data):
|
| 34 |
+
st.error(f"Invalid scenario format in {uploaded_file.name}.")
|
| 35 |
+
continue
|
| 36 |
+
else:
|
| 37 |
+
data = parse_zip_scenario(uploaded_file)
|
| 38 |
+
if data is None:
|
| 39 |
+
st.error(f"Invalid ZIP format in {uploaded_file.name}. Expected: resource_caps.csv, method_constraints.csv, method_costs.csv, allocations.csv")
|
| 40 |
+
continue
|
| 41 |
+
st.session_state.scenarios[uploaded_file.name] = data
|
| 42 |
+
except Exception as e:
|
| 43 |
+
st.error(f"Error loading {uploaded_file.name}: {e}")
|
| 44 |
+
|
| 45 |
+
def _prepare_comparison_data(selected):
|
| 46 |
+
"""Build comparison DataFrames from the currently selected scenarios.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
selected (list[str]): list of scenario names to include.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
| 53 |
+
df_summary — one row per scenario with total CO₂ and per-method values,
|
| 54 |
+
df_methods — long-format method allocation data,
|
| 55 |
+
df_resources — long-format resource usage data per method and scenario.
|
| 56 |
+
"""
|
| 57 |
+
all_methods = sorted({
|
| 58 |
+
m for name in selected
|
| 59 |
+
for m in st.session_state.scenarios[name].get("allocations", {})
|
| 60 |
+
})
|
| 61 |
+
all_resources = sorted({
|
| 62 |
+
r for name in selected
|
| 63 |
+
for r in st.session_state.scenarios[name].get("resource_caps", {})
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
summary_rows, method_alloc_rows, resource_usage_rows = [], [], []
|
| 67 |
+
|
| 68 |
+
for name in selected:
|
| 69 |
+
scenario = st.session_state.scenarios[name]
|
| 70 |
+
allocations = scenario.get("allocations", {})
|
| 71 |
+
method_costs = scenario.get("method_costs", {}) # ← fix: was using method_constraints
|
| 72 |
+
|
| 73 |
+
row = {
|
| 74 |
+
"Scenario": name,
|
| 75 |
+
f"CO₂ Removed ({CO2_UNIT})": scenario.get("summary", {}).get("total_co2_removed", 0),
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
for method in all_methods:
|
| 79 |
+
val = allocations.get(method, 0)
|
| 80 |
+
row[f"{method} ({CO2_UNIT})"] = val
|
| 81 |
+
method_alloc_rows.append({"Scenario": name, "Method": method, f"{CO2_UNIT}": val})
|
| 82 |
+
|
| 83 |
+
for resource in all_resources:
|
| 84 |
+
total_used = 0
|
| 85 |
+
for method, tco2 in allocations.items():
|
| 86 |
+
used = method_costs.get(method, {}).get(resource, 0) * tco2
|
| 87 |
+
total_used += used
|
| 88 |
+
if used > 0:
|
| 89 |
+
resource_usage_rows.append({
|
| 90 |
+
"Scenario": name, "Method": method,
|
| 91 |
+
"Resource": resource, "Used": used,
|
| 92 |
+
})
|
| 93 |
+
row[f"{resource} used"] = total_used
|
| 94 |
+
|
| 95 |
+
summary_rows.append(row)
|
| 96 |
+
|
| 97 |
+
return (
|
| 98 |
+
pd.DataFrame(summary_rows),
|
| 99 |
+
pd.DataFrame(method_alloc_rows),
|
| 100 |
+
pd.DataFrame(resource_usage_rows),
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _show_method_allocations(df):
|
| 105 |
+
"""Render a grouped bar chart of method allocations across scenarios.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
df (pd.DataFrame): long-format data with columns Scenario, Method, CO₂ value.
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
None
|
| 112 |
+
"""
|
| 113 |
+
if df.empty:
|
| 114 |
+
return
|
| 115 |
+
st.markdown("""<p class="crra-sub-heading">Method Allocations</p>""", unsafe_allow_html=True)
|
| 116 |
+
fig = px.bar(df, x="Method", y=f"{CO2_UNIT}", color="Scenario", barmode="group")
|
| 117 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 118 |
+
st.plotly_chart(fig, width='stretch')
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _show_resource_usage(df):
|
| 124 |
+
"""Render grouped bar charts of resource usage per method and total per scenario.
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
df (pd.DataFrame): long-format data with columns Scenario, Method, Resource, Used.
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
None
|
| 131 |
+
"""
|
| 132 |
+
if df.empty:
|
| 133 |
+
st.info("No resource usage data available (method_costs missing from scenarios?).")
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
st.markdown("""<p class="crra-sub-heading">Resource Use per Method</p>""", unsafe_allow_html=True)
|
| 137 |
+
for method in df["Method"].unique():
|
| 138 |
+
df_m = df[df["Method"] == method]
|
| 139 |
+
fig = px.bar(df_m, x="Resource", y="Used", color="Scenario", barmode="group", title=method)
|
| 140 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 141 |
+
st.plotly_chart(fig, width='stretch')
|
| 142 |
+
|
| 143 |
+
st.markdown("""<p class="crra-sub-heading">Total Resource Use per Scenario</p>""", unsafe_allow_html=True)
|
| 144 |
+
df_total = df.groupby(["Scenario", "Resource"]).sum(numeric_only=True).reset_index()
|
| 145 |
+
fig = px.bar(df_total, x="Scenario", y="Used", color="Resource", barmode="group")
|
| 146 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 147 |
+
st.plotly_chart(fig, width='stretch')
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def render_tab():
|
| 152 |
+
"""Render the Scenario Comparison tab.
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
None
|
| 156 |
+
"""
|
| 157 |
+
st.markdown('<h1 class="crra-heading">Scenario Comparison</h1>', unsafe_allow_html=True)
|
| 158 |
+
show_bar()
|
| 159 |
+
|
| 160 |
+
st.session_state.setdefault("scenarios", {})
|
| 161 |
+
|
| 162 |
+
panel_load_scenario()
|
| 163 |
+
|
| 164 |
+
if not st.session_state.scenarios:
|
| 165 |
+
st.info("No scenarios saved yet. Run an optimization in **Portfolio Optimization** tab or load a file above.")
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
# Select & manage scenarios
|
| 169 |
+
st.markdown("""<p class="crra-sub-heading">Select Scenarios to Compare</p>""", unsafe_allow_html=True)
|
| 170 |
+
scenario_names = list(st.session_state.scenarios.keys())
|
| 171 |
+
selected = st.multiselect("Select scenarios", scenario_names, default=scenario_names)
|
| 172 |
+
|
| 173 |
+
for name in selected:
|
| 174 |
+
col1, col2 = st.columns([10, 1])
|
| 175 |
+
with col1:
|
| 176 |
+
st.markdown(f"**{name}**")
|
| 177 |
+
with col2:
|
| 178 |
+
if st.button("❌", key=f"remove_{name}"):
|
| 179 |
+
st.session_state.scenarios.pop(name, None)
|
| 180 |
+
st.rerun()
|
| 181 |
+
|
| 182 |
+
if not selected:
|
| 183 |
+
st.warning("Please select at least one scenario.")
|
| 184 |
+
return
|
| 185 |
+
|
| 186 |
+
# Comparison
|
| 187 |
+
df_summary, df_methods, df_resources = _prepare_comparison_data(selected)
|
| 188 |
+
|
| 189 |
+
st.markdown("""<p class="crra-sub-heading">Summary Table</p>""", unsafe_allow_html=True)
|
| 190 |
+
st.dataframe(df_summary.style.format(na_rep="-", thousands=","), width='stretch')
|
| 191 |
+
st.download_button(
|
| 192 |
+
"Export Comparison CSV",
|
| 193 |
+
df_summary.to_csv(index=False).encode("utf-8"),
|
| 194 |
+
"scenario_comparison.csv", "text/csv",
|
| 195 |
+
icon=":material/download:"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
_show_method_allocations(df_methods)
|
| 199 |
_show_resource_usage(df_resources)
|
utils/data_utils.py
CHANGED
|
@@ -1,143 +1,235 @@
|
|
| 1 |
-
import io
|
| 2 |
-
import json
|
| 3 |
-
import re
|
| 4 |
-
import pandas as pd
|
| 5 |
-
import streamlit as st
|
| 6 |
-
import openpyxl
|
| 7 |
-
from openpyxl.styles import PatternFill, Font
|
| 8 |
-
from config.config import resource_unit_map, resource_order, method_order
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
if
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
return safe_constraints
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import openpyxl
|
| 7 |
+
from openpyxl.styles import PatternFill, Font
|
| 8 |
+
from config.config import resource_unit_map, resource_order, method_order, resource_groups, method_groups
|
| 9 |
+
|
| 10 |
+
def get_resource_to_group() -> dict[str, str]:
|
| 11 |
+
"""Map each resource name to its resource group.
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
dict[str, str]: resource name → group name (e.g. "Arable land" → "Land").
|
| 15 |
+
"""
|
| 16 |
+
return {r: g for g, resources in resource_groups.items() for r in resources}
|
| 17 |
+
|
| 18 |
+
def get_resource_to_unit() -> dict[str, str]:
|
| 19 |
+
"""Map each resource name to its physical unit.
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
dict[str, str]: resource name → unit (e.g. "Arable land" → "Mha").
|
| 23 |
+
"""
|
| 24 |
+
return {r: resource_unit_map[g] for g, resources in resource_groups.items() for r in resources}
|
| 25 |
+
|
| 26 |
+
def get_method_group(method: str) -> str:
|
| 27 |
+
"""Return the group a CDR method belongs to.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
method (str): CDR method name.
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
str: group name, or "Other" if not found.
|
| 34 |
+
"""
|
| 35 |
+
return next((g for g, methods in method_groups.items() if method in methods), "Other")
|
| 36 |
+
|
| 37 |
+
@st.cache_data
|
| 38 |
+
def load_slider_data():
|
| 39 |
+
"""Load and cache the cost slider configuration from data/cost_sliders.json.
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
dict: nested dict {method: {resource: {min, median, max}}}.
|
| 43 |
+
"""
|
| 44 |
+
with open("data/cost_sliders.json") as f:
|
| 45 |
+
data = json.load(f)
|
| 46 |
+
return data
|
| 47 |
+
|
| 48 |
+
def render_resource_input(name_clean, r, tooltip=None):
|
| 49 |
+
"""Render a number_input widget for a single resource availability cap.
|
| 50 |
+
|
| 51 |
+
Writes the entered value into st.session_state.resource_caps[r].
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
name_clean (str): display label (resource name without unit suffix).
|
| 55 |
+
r (str): canonical resource name used as the session state key.
|
| 56 |
+
tooltip (str | None): optional help text shown on hover.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
None
|
| 60 |
+
"""
|
| 61 |
+
widget_key = f"cap_{r}"
|
| 62 |
+
|
| 63 |
+
if widget_key not in st.session_state:
|
| 64 |
+
existing = st.session_state.resource_caps.get(r, 0.0)
|
| 65 |
+
st.session_state[widget_key] = existing if existing else None
|
| 66 |
+
|
| 67 |
+
cap = st.number_input(
|
| 68 |
+
label=name_clean,
|
| 69 |
+
min_value=0.0,
|
| 70 |
+
format="%.2f",
|
| 71 |
+
value=None,
|
| 72 |
+
placeholder="0.00",
|
| 73 |
+
key=widget_key,
|
| 74 |
+
help=tooltip
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
st.session_state.resource_caps[r] = cap if cap is not None else 0.0
|
| 78 |
+
|
| 79 |
+
def is_valid_scenario(scenario):
|
| 80 |
+
"""Check that a scenario dict has the minimum required keys to be loaded.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
scenario (dict): scenario data to validate.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
bool: True if all base keys are present, False otherwise.
|
| 87 |
+
"""
|
| 88 |
+
base_ok = (
|
| 89 |
+
isinstance(scenario, dict)
|
| 90 |
+
and "allocations" in scenario
|
| 91 |
+
and "resource_caps" in scenario
|
| 92 |
+
and "method_costs" in scenario
|
| 93 |
+
and "method_constraints" in scenario
|
| 94 |
+
and "summary" in scenario
|
| 95 |
+
)
|
| 96 |
+
if not base_ok:
|
| 97 |
+
return False
|
| 98 |
+
return True
|
| 99 |
+
|
| 100 |
+
def is_complete_scenario(scenario):
|
| 101 |
+
"""Check that a scenario carries full optimisation outputs, not just inputs.
|
| 102 |
+
|
| 103 |
+
A complete scenario can be displayed without re-running the optimisation.
|
| 104 |
+
An incomplete scenario (e.g. exported before resource_actual_gain was added)
|
| 105 |
+
triggers an automatic re-run on load.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
scenario (dict): scenario data to validate.
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
bool: True if the scenario includes all detailed output keys.
|
| 112 |
+
"""
|
| 113 |
+
required_extra = {
|
| 114 |
+
"resource_usage", "resource_used_total", "resource_remaining",
|
| 115 |
+
"resource_actual_gain", "enabled_methods",
|
| 116 |
+
}
|
| 117 |
+
return is_valid_scenario(scenario) and required_extra.issubset(scenario.keys())
|
| 118 |
+
|
| 119 |
+
def load_inputs_from_excel(file):
|
| 120 |
+
"""Parse an Excel workbook exported from the tool into session-state-ready dicts.
|
| 121 |
+
|
| 122 |
+
Expects sheets: resource_caps, constraints, coefficients.
|
| 123 |
+
Optional sheets: custom_resources, enabled_methods.
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
file: file-like object pointing to an .xlsx workbook.
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
tuple: (resource_caps, constraints, costs, custom_resources, enabled_methods, warnings)
|
| 130 |
+
where warnings is a list[str] of non-fatal import issues.
|
| 131 |
+
"""
|
| 132 |
+
xl = pd.read_excel(file, sheet_name=None)
|
| 133 |
+
|
| 134 |
+
resource_caps = {}
|
| 135 |
+
if "resource_caps" in xl:
|
| 136 |
+
for _, row in xl["resource_caps"].iterrows():
|
| 137 |
+
resource_caps[row["Resource"]] = float(row["Available Amount"])
|
| 138 |
+
|
| 139 |
+
constraints = {}
|
| 140 |
+
if "constraints" in xl:
|
| 141 |
+
for _, row in xl["constraints"].iterrows():
|
| 142 |
+
constraints[row["method"]] = {
|
| 143 |
+
"active": bool(row["active"]),
|
| 144 |
+
"cap_type": str(row["cap_type (percent or absolute)"]),
|
| 145 |
+
"cap_value": float(row["cap_value"])
|
| 146 |
+
}
|
| 147 |
+
costs = {}
|
| 148 |
+
if "coefficients" in xl:
|
| 149 |
+
for _, row in xl["coefficients"].iterrows():
|
| 150 |
+
m, r, v = row["method"], row["resource"], float(row["value"])
|
| 151 |
+
costs.setdefault(m, {})[r] = v
|
| 152 |
+
|
| 153 |
+
custom_resources = []
|
| 154 |
+
warnings = []
|
| 155 |
+
valid_resources = set(resource_order)
|
| 156 |
+
valid_methods = set(method_order)
|
| 157 |
+
if "custom_resources" in xl:
|
| 158 |
+
df_custom = xl["custom_resources"]
|
| 159 |
+
required_custom = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
|
| 160 |
+
if required_custom.issubset(df_custom.columns):
|
| 161 |
+
for bname in df_custom["name"].dropna().unique():
|
| 162 |
+
df_b = df_custom[df_custom["name"] == bname]
|
| 163 |
+
group = str(df_b["group"].iloc[0])
|
| 164 |
+
if group not in valid_resources:
|
| 165 |
+
warnings.append(f"New resource \"{bname}\" - group \"{group}\" not found: check spelling.")
|
| 166 |
+
continue
|
| 167 |
+
methods = {}
|
| 168 |
+
for _, row in df_b.iterrows():
|
| 169 |
+
m = str(row["method"])
|
| 170 |
+
if m not in valid_methods:
|
| 171 |
+
warnings.append(f"New resource \"{bname}\" - method \"{m}\" not found: check spelling.")
|
| 172 |
+
continue
|
| 173 |
+
methods[m] = {
|
| 174 |
+
"min": float(row["min"]),
|
| 175 |
+
"median": float(row["median"]),
|
| 176 |
+
"max": float(row["max"]),
|
| 177 |
+
}
|
| 178 |
+
custom_resources.append({
|
| 179 |
+
"name": str(bname),
|
| 180 |
+
"group": group,
|
| 181 |
+
"amount": float(df_b["amount"].iloc[0]),
|
| 182 |
+
"unit": str(df_b["unit"].iloc[0]),
|
| 183 |
+
"methods": methods,
|
| 184 |
+
})
|
| 185 |
+
|
| 186 |
+
# Per-resource manual method allocation (which CDR methods are allowed to
|
| 187 |
+
# use a given resource). Optional sheet for backward compatibility with
|
| 188 |
+
# older templates that predate this feature.
|
| 189 |
+
enabled_methods = {}
|
| 190 |
+
if "enabled_methods" in xl:
|
| 191 |
+
df_enabled = xl["enabled_methods"]
|
| 192 |
+
required_enabled = {"resource", "method", "enabled"}
|
| 193 |
+
if required_enabled.issubset(df_enabled.columns):
|
| 194 |
+
for _, row in df_enabled.iterrows():
|
| 195 |
+
r, m = row["resource"], row["method"]
|
| 196 |
+
enabled_methods.setdefault(r, {})[m] = bool(row["enabled"])
|
| 197 |
+
|
| 198 |
+
return resource_caps, constraints, costs, custom_resources, enabled_methods, warnings
|
| 199 |
+
|
| 200 |
+
def strip_unit_suffix(resource_name: str) -> str:
|
| 201 |
+
"""Remove trailing unit suffix from a resource name.
|
| 202 |
+
|
| 203 |
+
E.g. "Unspecified land (Mha/MtCO₂)" → "Unspecified land".
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
resource_name (str): raw resource name, possibly with a parenthetical unit.
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
str: resource name without the unit suffix.
|
| 210 |
+
"""
|
| 211 |
+
return re.sub(r"\s*\(.*?\)\s*$", "", resource_name).strip()
|
| 212 |
+
|
| 213 |
+
def neutralize_zero_coefficient_methods(method_costs: dict, method_constraints: dict) -> dict:
|
| 214 |
+
"""Return a copy of method_constraints with methods that have all-zero costs deactivated.
|
| 215 |
+
|
| 216 |
+
Prevents the LP from including methods with no usable resource coefficients.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
method_costs (dict): {method: {resource: coefficient}} cost matrix.
|
| 220 |
+
method_constraints (dict): {method: {active, cap_type, cap_value}}.
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
dict: modified copy of method_constraints with zero-cost methods deactivated.
|
| 224 |
+
"""
|
| 225 |
+
safe_constraints = {m: c.copy() for m, c in method_constraints.items()}
|
| 226 |
+
|
| 227 |
+
for method, constraint in safe_constraints.items():
|
| 228 |
+
if not constraint.get("active", True):
|
| 229 |
+
continue
|
| 230 |
+
coeffs = method_costs.get(method, {})
|
| 231 |
+
all_zero = all(v == 0 for v in coeffs.values()) if coeffs else True
|
| 232 |
+
if all_zero:
|
| 233 |
+
constraint["active"] = False
|
| 234 |
+
|
| 235 |
return safe_constraints
|
utils/ui_helpers.py
CHANGED
|
@@ -4,6 +4,15 @@ from config.config import CRRA_COLORS, CRRA_FONT, PLOTLY_TEMPLATE
|
|
| 4 |
|
| 5 |
|
| 6 |
def apply_chart_style(fig, height=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
fig.update_layout(
|
| 8 |
template=PLOTLY_TEMPLATE,
|
| 9 |
font=dict(family=CRRA_FONT, size=12, color=CRRA_COLORS["black"]),
|
|
@@ -20,12 +29,25 @@ def apply_chart_style(fig, height=None):
|
|
| 20 |
|
| 21 |
|
| 22 |
def register_chart_data(name: str, df: pd.DataFrame):
|
| 23 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
st.session_state.setdefault("chart_data_registry", {})
|
| 25 |
st.session_state["chart_data_registry"][name] = df.copy()
|
| 26 |
|
| 27 |
|
| 28 |
def show_bar():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
st.markdown(
|
| 30 |
"""
|
| 31 |
<div class="et_pb_module">
|
|
@@ -42,6 +64,17 @@ def show_bar():
|
|
| 42 |
|
| 43 |
|
| 44 |
def smart_display_format(val):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if abs(val) < 1e-8:
|
| 46 |
return ""
|
| 47 |
elif abs(val) < 0.001:
|
|
@@ -57,6 +90,17 @@ def smart_display_format(val):
|
|
| 57 |
|
| 58 |
|
| 59 |
def format_efficiency_summary(df: pd.DataFrame, df_default: pd.DataFrame):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
df_numeric = df.copy()
|
| 61 |
df_display = df_numeric.map(smart_display_format)
|
| 62 |
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
def apply_chart_style(fig, height=None):
|
| 7 |
+
"""Apply the standard CRRA theme (font, colours, margins) to a Plotly figure.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
fig: Plotly figure object.
|
| 11 |
+
height (int | None): optional fixed height in pixels.
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
fig: the same figure with updated layout.
|
| 15 |
+
"""
|
| 16 |
fig.update_layout(
|
| 17 |
template=PLOTLY_TEMPLATE,
|
| 18 |
font=dict(family=CRRA_FONT, size=12, color=CRRA_COLORS["black"]),
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def register_chart_data(name: str, df: pd.DataFrame):
|
| 32 |
+
"""Store a chart's underlying DataFrame in the session-level registry for bulk export.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
name (str): unique chart label used as the dict key and export sheet name.
|
| 36 |
+
df (pd.DataFrame): data behind the chart.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
None
|
| 40 |
+
"""
|
| 41 |
st.session_state.setdefault("chart_data_registry", {})
|
| 42 |
st.session_state["chart_data_registry"][name] = df.copy()
|
| 43 |
|
| 44 |
|
| 45 |
def show_bar():
|
| 46 |
+
"""Render the CRRA four-colour decorative bar below page headings.
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
None
|
| 50 |
+
"""
|
| 51 |
st.markdown(
|
| 52 |
"""
|
| 53 |
<div class="et_pb_module">
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def smart_display_format(val):
|
| 67 |
+
"""Format a numeric value with adaptive precision for table display.
|
| 68 |
+
|
| 69 |
+
Near-zero values are shown as an empty string; others use 2–5 decimal
|
| 70 |
+
places depending on magnitude.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
val (float): numeric value to format.
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
str: formatted string, or "" for near-zero values.
|
| 77 |
+
"""
|
| 78 |
if abs(val) < 1e-8:
|
| 79 |
return ""
|
| 80 |
elif abs(val) < 0.001:
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
def format_efficiency_summary(df: pd.DataFrame, df_default: pd.DataFrame):
|
| 93 |
+
"""Build a styled Pandas Styler for the resource-efficiency coefficient summary table.
|
| 94 |
+
|
| 95 |
+
Cells whose value differs from the default median are highlighted in red.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
df (pd.DataFrame): current coefficient values (methods × resources).
|
| 99 |
+
df_default (pd.DataFrame): default median values for the same shape.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
pandas.io.formats.style.Styler: styled dataframe ready for st.dataframe().
|
| 103 |
+
"""
|
| 104 |
df_numeric = df.copy()
|
| 105 |
df_display = df_numeric.map(smart_display_format)
|
| 106 |
|