jeffrey1963 commited on
Commit
39cf09a
Β·
verified Β·
1 Parent(s): 6c90dc4

Upload 9 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Carson_map[[:space:]](6).png filter=lfs diff=lfs merge=lfs -text
Carson_map (6).png ADDED

Git LFS Details

  • SHA256: 529fde5e9033c127a09e3c5f547d7941c77774548f2a300d0244b0e6323d1e5f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
R6_global (3).py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # R6_global.py
3
+ class SimState:
4
+ def __init__(self):
5
+ self.round_counter = 1
6
+ self.parcel_dict = None
7
+ self.health_tracking = {}
8
+ def update_round(self):
9
+ self.round_counter += 1
10
+ # βœ… Global singleton instance
11
+ sim_state = SimState()
README (2).md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Cattle-Elk R6 Global
3
+ emoji: πŸš€
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 5.25.2
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
Sim_Engine (12).py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def initialize_parcels(parcel_map, cluster_labels):
2
+ import numpy as np
3
+
4
+ if parcel_map is None or np.size(parcel_map) == 0:
5
+ raise ValueError("🚨 parcel_map is empty or None β€” check your input wiring!")
6
+
7
+ parcel_map = np.array(parcel_map)
8
+
9
+ if parcel_map.ndim != 2:
10
+ raise ValueError(f"🚨 parcel_map must be 2D β€” got shape {parcel_map.shape}")
11
+
12
+ parcel_dict = {} # βœ… This is the fix
13
+
14
+ for i in range(parcel_map.shape[0]):
15
+ for j in range(parcel_map.shape[1]):
16
+ cluster_id = parcel_map[i, j]
17
+ land_type = cluster_labels.get(cluster_id, "Unknown")
18
+
19
+ parcel_dict[(i, j)] = {
20
+ "land_type": land_type,
21
+ "forage": None,
22
+ "health": 1.0,
23
+ "degraded": False,
24
+ "cattle_grazing": {
25
+ "conservative": land_type == "Productive Grass",
26
+ "moderate": land_type in ["Productive Grass", "Pasture/Desert"],
27
+ "aggressive": land_type not in ["Water"]
28
+ },
29
+ "elk_grazing": {
30
+ "default": land_type in ["Riparian Sensitive Zone", "Productive Grass"]
31
+ }
32
+ }
33
+ return parcel_dict
34
+
35
+ def get_land_forage_rates():
36
+ return {
37
+ 'Productive Grass': 1.0,
38
+ 'Pasture/Desert': 0.4,
39
+ 'Riparian Sensitive Zone': 1.2,
40
+ 'Rocky Area': 0.2,
41
+ 'Water': 0.0
42
+ }
43
+
44
+
45
+ def assign_initial_forage(parcel_dict, land_forage_rates):
46
+ for parcel in parcel_dict.values():
47
+ rate = land_forage_rates.get(parcel["land_type"], 0.0)
48
+ parcel["forage"] = rate * 100 # initial AUMs
49
+
50
+ import numpy as np
51
+ import numpy as np
52
+ import matplotlib.pyplot as plt
53
+ from collections import Counter
54
+ from matplotlib.colors import ListedColormap
55
+ import matplotlib.patches as mpatches
56
+
57
+ def simulate_period(parcel_dict, grazing_strategy="moderate", cattle_stocking_rate=8000, elk_pressure=10000):
58
+ print(f"\n🟒 Running LP-based simulation using grazing strategy: **{grazing_strategy.upper()}**")
59
+ print(f"🧠 simulate_period β€” parcel_dict id = {id(parcel_dict)}")
60
+
61
+ for (i, j), parcel in parcel_dict.items():
62
+ print(f"Parcel ({i},{j}) - forage before grazing : {parcel['forage']:.1f}, health: {parcel['health']:.2f}")
63
+
64
+ import numpy as np
65
+ from scipy.optimize import linprog
66
+
67
+ keys = list(parcel_dict.keys())
68
+ n_rows = max(i for i, _ in keys) + 1
69
+ n_cols = max(j for _, j in keys) + 1
70
+ num_cells = n_rows * n_cols
71
+
72
+ # Step 1: Regrowth
73
+ land_growth_rates = {
74
+ 'Productive Grass': 1.0,
75
+ 'Pasture/Desert': 0.4,
76
+ 'Riparian Sensitive Zone': 1.2,
77
+ 'Rocky Area': 0.2,
78
+ 'Water': 0.0
79
+ }
80
+ for parcel in parcel_dict.values():
81
+ base_growth = land_growth_rates.get(parcel["land_type"], 0.0)
82
+ weather = np.random.normal(1.0, 0.15)
83
+ regrowth = base_growth * weather * 1
84
+ regrowth *= parcel["health"]
85
+ parcel["forage"] = min(parcel["forage"] + regrowth, 100)
86
+
87
+ # Step 2: Subtract uniform elk grazing from all parcels
88
+ elk_grazing_per_parcel = elk_pressure / num_cells
89
+ for parcel in parcel_dict.values():
90
+ parcel["forage"] -= elk_grazing_per_parcel
91
+ parcel["forage"] = max(parcel["forage"], 0.0)
92
+
93
+ # Step 3: LP for cattle
94
+ cost = []
95
+ bounds = []
96
+ eligible_keys = []
97
+ for i in range(n_rows):
98
+ for j in range(n_cols):
99
+ p = parcel_dict[(i, j)]
100
+ if not p["cattle_grazing"].get(grazing_strategy, False):
101
+ cost.append(0)
102
+ bounds.append((0, 0))
103
+ continue
104
+ if grazing_strategy in {"conservative", "moderate"} and p["land_type"] == "Riparian Sensitive Zone":
105
+ cost.append(0)
106
+ bounds.append((0, 0))
107
+ continue
108
+ cost.append((i + j) * 0.02)
109
+ bounds.append((0, p["forage"]))
110
+ eligible_keys.append((i, j))
111
+
112
+ A_eq = [1.0 if b[1] > 0 else 0.0 for b in bounds]
113
+ b_eq = [cattle_stocking_rate]
114
+
115
+ result = linprog(c=cost, A_eq=[A_eq], b_eq=b_eq, bounds=bounds, method="highs")
116
+ if not result.success:
117
+ raise RuntimeError("Grazing LP failed: " + result.message)
118
+
119
+ grazing_values = result.x
120
+
121
+ # Step 4: Apply grazing and your specified health rule
122
+ for idx, ((i, j), x) in enumerate(zip(parcel_dict.keys(), grazing_values)):
123
+ parcel = parcel_dict[(i, j)]
124
+ parcel["forage"] -= x
125
+
126
+ # βœ… Your health rule (fully time-dynamic)
127
+ if parcel["forage"] <= 0:
128
+ parcel["health"] = max(parcel["health"] - 0.25, 0.0)
129
+ elif parcel["forage"] < 20:
130
+ parcel["health"] = max(parcel["health"] - 0.1, 0.0)
131
+ else:
132
+ parcel["health"] = min(parcel["health"] + 0.02, 1.0)
133
+
134
+ print("\n🧠 HEALTH + FORAGE SNAPSHOT")
135
+ for (i, j), parcel in parcel_dict.items():
136
+ print(f"Parcel ({i},{j}) - forage after grazing : {parcel['forage']:.1f}, health: {parcel['health']:.2f}")
137
+
138
+
139
+ def simulate_periodold(parcel_dict, grazing_strategy="moderate", cattle_stocking_rate=5000, elk_pressure=3000):
140
+ print(f"\n🟒 Running simulation using cattle grazing strategy: **{grazing_strategy.upper()}**")
141
+
142
+ land_growth_rates = {
143
+ 'Productive Grass': 1.0,
144
+ 'Pasture/Desert': 0.4,
145
+ 'Riparian Sensitive Zone': 1.2,
146
+ 'Rocky Area': 0.2,
147
+ 'Water': 0.0
148
+ }
149
+
150
+ # 1. Simulate forage regrowth for 8 months
151
+ for parcel in parcel_dict.values():
152
+ base_growth = land_growth_rates.get(parcel["land_type"], 0.0)
153
+ weather = np.random.normal(1.0, 0.15)
154
+ regrowth = base_growth * weather * 8 # ← 8 months, as you said
155
+ regrowth *= parcel["health"] # degrade means slower regrowth
156
+ parcel["forage"] = min(parcel["forage"] + regrowth, 100)
157
+
158
+ # 2. Count eligible parcels
159
+ total_grazed_parcels = sum(
160
+ 1 for parcel in parcel_dict.values() if parcel["cattle_grazing"].get(grazing_strategy, False)
161
+ )
162
+ if total_grazed_parcels == 0:
163
+ print("⚠️ No parcels match the selected grazing strategy.")
164
+ return
165
+
166
+ cattle_grazing_per_parcel = cattle_stocking_rate / total_grazed_parcels
167
+ elk_grazing_per_parcel = elk_pressure / len(parcel_dict)
168
+
169
+ # 3. Simulate grazing and degradation
170
+ for parcel in parcel_dict.values():
171
+ if not parcel["cattle_grazing"].get(grazing_strategy, False):
172
+ continue
173
+
174
+ total_grazing = cattle_grazing_per_parcel + elk_grazing_per_parcel
175
+
176
+ if total_grazing > parcel["forage"]:
177
+ parcel["degraded"] = True
178
+ parcel["health"] = max(parcel["health"] - 0.1, 0.0)
179
+ else:
180
+ parcel["health"] = min(parcel["health"] + 0.02, 1.0)
181
+
182
+ parcel["forage"] = max(parcel["forage"] - total_grazing, 0)
183
+
184
+ def get_forage_map(parcel_dict, n_rows, n_cols):
185
+ return np.array([[parcel_dict[(i, j)]["forage"] for j in range(n_cols)] for i in range(n_rows)])
186
+
187
+ def get_health_map(parcel_dict, n_rows, n_cols):
188
+ """
189
+ Returns a 2D numpy array representing the health of each parcel.
190
+ """
191
+ return np.array([[parcel_dict[(i, j)]["health"] for j in range(n_cols)] for i in range(n_rows)])
192
+
193
+
194
+ def plot_health_map(health_map, title="Parcel Health Levels", save_path=None):
195
+ """
196
+ Plots a heatmap of the parcel health values.
197
+ """
198
+ import matplotlib.pyplot as plt
199
+
200
+ plt.figure(figsize=(8, 6))
201
+ plt.imshow(health_map, cmap='RdYlGn', origin='upper', vmin=0, vmax=1)
202
+ plt.colorbar(label="Health Index (0–1)")
203
+ plt.title(title)
204
+ plt.axis('off')
205
+ plt.tight_layout()
206
+ if save_path:
207
+ plt.savefig(save_path)
208
+ plt.show()
209
+
210
+
211
+
212
+ def plot_forage_map(forage_map, title="Parcel Forage Levels"):
213
+ plt.figure(figsize=(8, 6))
214
+ plt.imshow(forage_map, cmap='YlGn', origin='upper')
215
+ plt.colorbar(label="Forage AUMs")
216
+ plt.title(title)
217
+ plt.axis('off')
218
+ plt.tight_layout()
219
+ plt.show()
220
+
221
+ def run_full_simulation(parcel_map, cluster_labels, n_rows, n_cols, strategy="moderate"):
222
+ parcel_dict = initialize_parcels(parcel_map, cluster_labels)
223
+ land_forage_rates = get_land_forage_rates()
224
+ assign_initial_forage(parcel_dict, land_forage_rates)
225
+ simulate_period(parcel_dict, grazing_strategy=strategy)
226
+ return parcel_dict
Sim_Setup_Fcns (8).py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+
3
+ def load_and_crop_image(path="Carson_map.png", crop_box=(15, 15, 1000, 950)):
4
+ img = Image.open(path).convert("RGB")
5
+ cropped_img = img.crop(crop_box)
6
+ return cropped_img
7
+
8
+
9
+ from sklearn.cluster import KMeans
10
+ import numpy as np
11
+
12
+ def cluster_image(cropped_img, n_clusters=6):
13
+ img_array = np.array(cropped_img)
14
+ pixels = img_array.reshape(-1, 3)
15
+ kmeans = KMeans(n_clusters=n_clusters, random_state=42).fit(pixels)
16
+ labels = kmeans.labels_.reshape(img_array.shape[:2])
17
+ return labels
18
+
19
+
20
+ from collections import Counter
21
+ import numpy as np
22
+
23
+ def build_parcel_map(clustered_img, grid_size=20):
24
+ height, width = clustered_img.shape
25
+ n_rows = height // grid_size
26
+ n_cols = width // grid_size
27
+ parcel_map = np.zeros((n_rows, n_cols), dtype=int)
28
+
29
+ for i in range(n_rows):
30
+ for j in range(n_cols):
31
+ patch = clustered_img[i*grid_size:(i+1)*grid_size, j*grid_size:(j+1)*grid_size].flatten()
32
+ dominant = Counter(patch).most_common(1)[0][0]
33
+ parcel_map[i, j] = dominant
34
+
35
+ return parcel_map, n_rows, n_cols
36
+
37
+
38
+ import matplotlib.pyplot as plt
39
+ import matplotlib.patches as mpatches
40
+ from matplotlib.colors import ListedColormap
41
+
42
+ def plot_parcel_map(parcel_map, cluster_labels, land_colors, title="25Γ—25 Land Parcels by Land Type"):
43
+ cmap = ListedColormap(land_colors)
44
+ plt.figure(figsize=(10, 8))
45
+ plt.imshow(parcel_map, cmap=cmap, origin='upper')
46
+ legend_patches = [mpatches.Patch(color=land_colors[i], label=cluster_labels[i]) for i in cluster_labels]
47
+ plt.legend(handles=legend_patches, bbox_to_anchor=(1.05, 1), loc='upper left', title="Land Type")
48
+ plt.title(title)
49
+ plt.axis('off')
50
+ plt.tight_layout()
51
+ plt.show()
52
+
53
+ def plot_parcel_map_to_file(parcel_map, cluster_labels, land_colors, save_path="clustered_map.png", title="25Γ—25 Land Parcels by Land Type"):
54
+ cmap = ListedColormap(land_colors)
55
+ fig, ax = plt.subplots(figsize=(10, 8))
56
+ cax = ax.imshow(parcel_map, cmap=cmap, origin='upper')
57
+ legend_patches = [mpatches.Patch(color=land_colors[i], label=cluster_labels[i]) for i in cluster_labels]
58
+ ax.legend(handles=legend_patches, bbox_to_anchor=(1.05, 1), loc='upper left', title="Land Type")
59
+ ax.set_title(title)
60
+ ax.axis('off')
61
+ plt.tight_layout()
62
+ plt.savefig(save_path)
63
+ plt.close(fig)
64
+
65
+
66
+ def get_cluster_labels():
67
+ return {
68
+ 0: 'Pasture/Desert',
69
+ 1: 'Productive Grass',
70
+ 2: 'Pasture/Desert',
71
+ 3: 'Riparian Sensitive Zone',
72
+ 4: 'Rocky Area',
73
+ 5: 'Water'
74
+ }
75
+
76
+
77
+ def get_land_colors():
78
+ return ['#dfb867', '#a0ca76', '#dfb867', '#5b8558', '#888888', '#3a75a8']
79
+
80
+
81
+
82
+
83
+
app (26).py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import numpy as np
4
+ from openai import OpenAI
5
+ from Sim_Setup_Fcns import (
6
+ load_and_crop_image, cluster_image, build_parcel_map,
7
+ get_cluster_labels, get_land_colors, plot_parcel_map_to_file
8
+ )
9
+ from Sim_Engine import run_full_simulation
10
+ from feedback_fcns import (
11
+ summarize_initial_conditions, plot_forage_map_to_file,
12
+ elk_feedback, usfs_feedback, simulate_and_summarize, full_response
13
+ )
14
+
15
+ from zone_utils import (
16
+ identify_zones, plot_labeled_zones,
17
+ assign_zone_labels, save_zone_info_to_excel,override_zone_id_and_label
18
+ )
19
+ from R6_global import sim_state
20
+ #sim_state = SimState() # βœ… Create it ONCE here, globally
21
+ #if sim_state.parcel_dict is None:
22
+ # from Sim_Engine import initialize_parcels, assign_initial_forage, get_land_forage_rates
23
+ # sim_state.parcel_dict = initialize_parcels(parcel_map, cluster_labels)
24
+ # assign_initial_forage(sim_state.parcel_dict, get_land_forage_rates())
25
+
26
+ # === Setup on Launch ===
27
+ img = load_and_crop_image("Carson_map.png")
28
+ clustered_img = cluster_image(img)
29
+ parcel_map, n_rows, n_cols = build_parcel_map(clustered_img)
30
+ cluster_labels = get_cluster_labels()
31
+ land_colors = get_land_colors()
32
+ plot_parcel_map_to_file(parcel_map, cluster_labels, land_colors, save_path="clustered_map.png")
33
+
34
+ # === Zoning ===
35
+
36
+ # 1. Identify contiguous zones
37
+ zone_map, zone_to_cluster = identify_zones(parcel_map, connectivity="queen")
38
+
39
+ # 2. Assign human-readable labels (before override)
40
+ zone_labels = assign_zone_labels(zone_to_cluster)
41
+ # === Manual override for mislabeled riparian zone ===
42
+ # First, update the zone label once
43
+ for zid, lbl in zone_labels.items():
44
+ if lbl == "A" and zone_to_cluster[zid] == 1:
45
+ zone_labels[zid] = "Riparian A1"
46
+ if lbl == "M" and zone_to_cluster[zid] == 1:
47
+ zone_labels[zid] = "Riparian A2"
48
+ # Then update all matching parcels
49
+ for i in range(n_rows):
50
+ for j in range(n_cols):
51
+ zone_id = zone_map[i, j]
52
+ if zone_labels.get(zone_id) == "Riparian A1":
53
+ parcel_map[i, j] = 2
54
+ if zone_labels.get(zone_id) == "Riparian A2":
55
+ parcel_map[i, j] = 2
56
+
57
+ #
58
+
59
+
60
+ # ⬇️ Add this block right after the override
61
+ zone_to_cluster = {}
62
+ for zone_id in np.unique(zone_map):
63
+ indices = np.argwhere(zone_map == zone_id)
64
+ if len(indices) > 0:
65
+ i, j = indices[0]
66
+ zone_to_cluster[zone_id] = parcel_map[i, j]
67
+
68
+ # 6. Plot labeled zones after override and mapping
69
+ plot_labeled_zones(zone_map, zone_labels, zone_to_cluster, save_path="zones_labeled.png")
70
+
71
+ # 5. Define cluster-to-class mapping (should stay after override)
72
+ cluster_to_class = {
73
+ 0: "desert",
74
+ 1: "pasture",
75
+ 2: "riparain",
76
+ 3: "sensitive riparian",
77
+ 4: "wetland",
78
+ 5: "water"
79
+ }
80
+
81
+ # 7. Save zone info to Excel
82
+ zone_excel_path = "zone_info.xlsx"
83
+ save_zone_info_to_excel(
84
+ parcel_map, zone_map, zone_labels, zone_to_cluster, cluster_to_class,
85
+ save_path=zone_excel_path
86
+ )
87
+
88
+
89
+ client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
90
+
91
+ # === Gradio App ===
92
+ with gr.Blocks() as demo:
93
+ gr.Markdown("# AGEC 3052 β€” Grazing Strategy Simulation")
94
+ gr.Image(value="clustered_map.png", label="Initial 25Γ—25 Parcel Layout")
95
+ gr.Image(value="zones_labeled.png", label="Labeled Pasture & Riparian Zones")
96
+ # βœ… Downloadable Excel
97
+ gr.File(value=zone_excel_path, label="Download Zone Info (Excel)")
98
+
99
+ plan = gr.Radio(["Conservative", "Normal", "Aggressive"], label="Grazing Plan")
100
+ essay = gr.Textbox(lines=8, label="Your Essay Justifying the Plan")
101
+
102
+ elk_output = gr.Textbox(label="Elk Stakeholder Feedback")
103
+ usfs_output = gr.Textbox(label="USFS Feedback")
104
+ sim_output = gr.Textbox(label="Simulation Results", lines=2)
105
+ sim_image = gr.Image(label="Forage Map After Simulation", type="filepath")
106
+ health_image = gr.Image(label="Health Map After Simulation", type="filepath")
107
+
108
+ round_counter = gr.State(value=1)
109
+ history = gr.State(value=[summarize_initial_conditions(n_rows, n_cols)])
110
+
111
+ from R6_global import sim_state
112
+ # sim_state = SimState() # βœ… Define this at the top of app.py
113
+
114
+ parcel_map_state = gr.State(value=parcel_map)
115
+ cluster_labels_state = gr.State(value=cluster_labels)
116
+ n_rows_state = gr.State(value=n_rows)
117
+ n_cols_state = gr.State(value=n_cols)
118
+
119
+ def submit_handler(plan_choice, essay_text, parcel_map, cluster_labels, n_rows, n_cols, history_val):
120
+ from R6_global import sim_state
121
+ if sim_state.parcel_dict is None:
122
+ from Sim_Engine import initialize_parcels, assign_initial_forage, get_land_forage_rates
123
+ sim_state.parcel_dict = initialize_parcels(parcel_map, cluster_labels)
124
+ assign_initial_forage(sim_state.parcel_dict, get_land_forage_rates())
125
+
126
+ parcel_dict = sim_state.parcel_dict
127
+ round_counter = sim_state.round_counter
128
+
129
+ return full_response(plan_choice, essay_text, history_val[-1])
130
+
131
+
132
+ # def sim_handler(plan_choice, round_val, history_val):
133
+ # summary, map_path, health_path, new_round = simulate_and_summarize(
134
+ # plan_choice, round_val, parcel_map, cluster_labels, n_rows, n_cols,
135
+ # run_full_simulation, history_val
136
+ # )
137
+ # history_val.append(summary)
138
+ def sim_handler(plan_choice, round_val, history_val):
139
+ from R6_global import sim_state
140
+ # sim_state = SimState()
141
+
142
+ print(f"DEBUG: sim_state.parcel_dict is {type(sim_state.parcel_dict)}")
143
+
144
+ # Step 1: Initialize parcel_dict if it's missing
145
+ if sim_state.parcel_dict is None:
146
+ from Sim_Engine import initialize_parcels, assign_initial_forage, get_land_forage_rates
147
+ print("DEBUG: Initializing parcel_dict inside sim_handler")
148
+
149
+ sim_state.parcel_dict = initialize_parcels(parcel_map, cluster_labels)
150
+ assign_initial_forage(
151
+ sim_state.parcel_dict,
152
+ get_land_forage_rates()
153
+ )
154
+
155
+ # Ensure it's initialized
156
+ assert sim_state.parcel_dict is not None, "🚨 sim_handler: parcel_dict is STILL None after attempted init"
157
+
158
+ # Step 2: Run the simulation
159
+ summary, map_path, health_path, new_round, updated_parcel_dict = simulate_and_summarize(
160
+ plan_choice, round_val, parcel_map, cluster_labels, n_rows, n_cols,
161
+ run_full_simulation, history_val
162
+ )
163
+ sim_state.parcel_dict = updated_parcel_dict # βœ… Store the updated parcel dict
164
+
165
+
166
+ # Step 3: Manually increment round and return
167
+ sim_state.round_counter += 1
168
+ print(f"DEBUG: Updated round = {sim_state.round_counter}")
169
+ history_val.append(summary)
170
+ return summary, map_path, health_path, sim_state.round_counter, history_val
171
+
172
+
173
+ submit_btn = gr.Button("Submit Grazing Plan")
174
+ sim_btn = gr.Button("Run Simulation")
175
+
176
+ # submit_btn.click(fn=submit_handler, inputs=[plan, essay, history], outputs=[elk_output, usfs_output])
177
+ submit_btn.click(
178
+ fn=submit_handler,
179
+ inputs=[plan, essay, parcel_map_state, cluster_labels_state, n_rows_state, n_cols_state, history],
180
+ outputs=[elk_output, usfs_output]
181
+ )
182
+
183
+
184
+ sim_btn.click(fn=sim_handler, inputs=[plan, round_counter, history], outputs=[sim_output, sim_image, health_image, round_counter, history])
185
+
186
+ demo.launch()
feedback_fcns (11).py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ from openai import OpenAI
6
+ from Sim_Engine import simulate_period
7
+
8
+ client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
9
+
10
+ def summarize_initial_conditions(n_rows, n_cols):
11
+ from R6_global import sim_state
12
+ # sim_state = SimState()
13
+ num_parcels = n_rows * n_cols
14
+ avg_forage = round(random.uniform(6.5, 8.0), 1)
15
+ degraded_pct = round(random.uniform(0.0, 2.0), 1)
16
+ riparian_health = random.choice(["excellent", "moderate", "fragile"])
17
+ elk_corridor_status = random.choice([
18
+ "completely intact and lightly used",
19
+ "intact but under slight pressure from cattle movement",
20
+ "showing signs of fragmentation near key crossings"
21
+ ])
22
+ rainfall_outlook = random.choice(["normal", "below average", "above average"])
23
+
24
+ return (
25
+ f"There are {num_parcels} parcels in total. Grazing has not yet occurred.\n"
26
+ f"Average forage availability is {avg_forage} AUMs per parcel, with about {degraded_pct}% of land already degraded due to prior conditions.\n"
27
+ f"Riparian zone condition is {riparian_health}, and the elk movement corridor is {elk_corridor_status}.\n"
28
+ f"The seasonal rainfall outlook is {rainfall_outlook}."
29
+ )
30
+
31
+ def plot_forage_map_to_file(parcel_dict, n_rows, n_cols, title="Forage Map", save_path="forage_map.png"):
32
+ forage_map = np.array([
33
+ [parcel_dict[(i, j)]["forage"] for j in range(n_cols)]
34
+ for i in range(n_rows)
35
+ ])
36
+ fig, ax = plt.subplots(figsize=(8, 6))
37
+ cax = ax.imshow(forage_map, cmap='YlGn', origin='upper')
38
+ fig.colorbar(cax, label="Forage AUMs")
39
+ ax.set_title(title)
40
+ ax.axis('off')
41
+ plt.tight_layout()
42
+ plt.savefig(save_path)
43
+ plt.close(fig)
44
+
45
+ def elk_feedback(plan_choice, current_summary):
46
+ prompt = f"""
47
+ You represent a coalition of elk-related interests: conservationists, hunting advocates, and the hospitality/lodging industry.
48
+ A student has selected the **'{plan_choice}'** cattle grazing strategy. Below are the current ecological conditions:
49
+ -----
50
+ {current_summary}
51
+ -----
52
+ Please do the following:
53
+ - Explicitly choose **one elk management strategy** from the list:
54
+ - **Preserve**: strict elk protections, no hunting, unrestricted movement
55
+ - **Cooperate**: shared use corridor, some riparian restrictions, sustainable elk population
56
+ - **Exploit**: prioritize hunting/tourism, tolerate reduced elk numbers and access
57
+ - Reflect each group's view briefly, but unify the final position.
58
+ - Justify your strategy choice based on the above ecological indicators.
59
+ """
60
+ response = client.chat.completions.create(
61
+ model="gpt-3.5-turbo",
62
+ messages=[{"role": "user", "content": prompt}],
63
+ temperature=0.8
64
+ )
65
+ return response.choices[0].message.content
66
+
67
+ def usfs_feedback(plan_choice, student_essay, current_summary):
68
+ # Extract the AUM line from the summary
69
+ aum_line = ""
70
+ for line in current_summary.split("\n"):
71
+ if "Average forage availability" in line:
72
+ aum_line = line.strip()
73
+ break
74
+
75
+ prompt = f"""
76
+ You are a USFS land management agent evaluating a student’s cattle grazing proposal.
77
+
78
+ The student selected the **'{plan_choice}'** strategy and submitted this justification:
79
+ -----
80
+ {student_essay}
81
+ -----
82
+
83
+ Here are the current rangeland conditions:
84
+ -----
85
+ {current_summary}
86
+ -----
87
+
88
+ 🚨 **Must Include Block**:
89
+ You must include this sentence exactly in your response:
90
+ β†’ "{aum_line}"
91
+
92
+ Then provide your evaluation:
93
+ - Comment on forage, degradation, riparian and corridor health
94
+ - State whether the plan is ecologically sound
95
+ - Suggest improvements if needed
96
+ - Keep the tone professional, clear, and grounded in the data
97
+ """
98
+
99
+ response = client.chat.completions.create(
100
+ model="gpt-3.5-turbo",
101
+ messages=[{"role": "user", "content": prompt}],
102
+ temperature=0.7
103
+ )
104
+ return response.choices[0].message.content
105
+
106
+ def simulate_and_summarizeold(plan_choice, round_counter, parcel_dict, parcel_map, cluster_labels, n_rows, n_cols, elk_pressure):
107
+ # Interpret strategy
108
+ strategy_map = {
109
+ "conservative": "conservative",
110
+ "normal": "moderate",
111
+ "aggressive": "aggressive"
112
+ }
113
+ strategy = strategy_map.get(plan_choice.lower(), "moderate")
114
+
115
+ # βœ… Reuse incoming parcel_dict β€” do not reset
116
+ print(f"πŸ” simulate_and_summarize β€” calling simulate_period on id={id(parcel_dict)}")
117
+ simulate_period(parcel_dict, grazing_strategy=strategy, elk_pressure=elk_pressure)
118
+
119
+ # Extract summary
120
+ forage_vals = [p["forage"] for p in parcel_dict.values()]
121
+ avg_forage = sum(forage_vals) / len(forage_vals)
122
+ degraded_pct = 100 * sum(p["health"] < 0.5 for p in parcel_dict.values()) / len(parcel_dict)
123
+
124
+ summary = (
125
+ f"After Round {round_counter} with the '{plan_choice}' plan:\n"
126
+ f"Avg forage: {avg_forage:.1f} AUMs, Parcels with low health (<0.5): {degraded_pct:.1f}%"
127
+ )
128
+
129
+ # Generate map visuals
130
+ from Sim_Engine import get_forage_map, get_health_map, plot_forage_map, plot_health_map
131
+ forage_map = get_forage_map(parcel_dict, n_rows, n_cols)
132
+ health_map = get_health_map(parcel_dict, n_rows, n_cols)
133
+ plot_forage_map(forage_map, title=f"Forage Map (Round {round_counter})", save_path="forage_map.png")
134
+ plot_health_map(health_map, title=f"Health Map (Round {round_counter})", save_path="health_map.png")
135
+
136
+ return summary, "forage_map.png", "health_map.png", round_counter + 1, parcel_dict
137
+ from R6_global import SimState
138
+ from Sim_Engine import initialize_parcels, assign_initial_forage, get_land_forage_rates
139
+
140
+ def simulate_and_summarize(plan_choice, round_counter, parcel_map, cluster_labels, n_rows, n_cols, run_full_simulation, history):
141
+ from R6_global import sim_state
142
+ from Sim_Engine import initialize_parcels, assign_initial_forage, get_land_forage_rates, simulate_period, get_health_map, plot_health_map
143
+ from feedback_fcns import plot_forage_map_to_file
144
+
145
+ if sim_state.parcel_dict is None:
146
+ print("πŸ›  simulate_and_summarize: initializing parcel_dict inside feedback_fcns.py")
147
+ sim_state.parcel_dict = initialize_parcels(parcel_map, cluster_labels)
148
+ assign_initial_forage(sim_state.parcel_dict, get_land_forage_rates())
149
+
150
+ parcel_dict = sim_state.parcel_dict
151
+ round_counter = sim_state.round_counter
152
+
153
+ if parcel_dict is None:
154
+ raise ValueError("❌ parcel_dict is None β€” it must be initialized before calling simulate_and_summarize.")
155
+
156
+ strategy_map = {
157
+ "conservative": "conservative",
158
+ "normal": "moderate",
159
+ "aggressive": "aggressive"
160
+ }
161
+ strategy = strategy_map.get(plan_choice.lower())
162
+
163
+ # βœ… Run the simulation and print parcel_dict ID
164
+ print(f"πŸ” simulate_and_summarize β€” calling simulate_period on id={id(parcel_dict)}")
165
+ simulate_period(parcel_dict, grazing_strategy=strategy)
166
+
167
+ # βœ… Plot updated forage and health maps
168
+ plot_forage_map_to_file(parcel_dict, n_rows, n_cols, title=f"Round {round_counter} Forage Map")
169
+ health_map = get_health_map(parcel_dict, n_rows, n_cols)
170
+ plot_health_map(health_map, title=f"Round {round_counter} Health Map", save_path="health_map.png")
171
+
172
+ # βœ… Calculate summary
173
+ forage_vals = [p["forage"] for p in parcel_dict.values()]
174
+ avg_forage = sum(forage_vals) / len(forage_vals)
175
+ degraded_pct = 100 * sum(p["degraded"] for p in parcel_dict.values()) / len(parcel_dict)
176
+
177
+ summary = (
178
+ f"After Round {round_counter} with the '{plan_choice}' plan:\n"
179
+ f"Avg forage: {avg_forage:.1f} AUMs, Degraded parcels: {degraded_pct:.1f}%"
180
+ )
181
+
182
+ # βœ… Save state for next round
183
+ sim_state.parcel_dict = parcel_dict
184
+
185
+ return summary, "forage_map.png", "health_map.png", sim_state.round_counter, sim_state.parcel_dict
186
+
187
+
188
+ def simulate_and_summarizeoldapr19(plan_choice, round_counter, parcel_map, cluster_labels, n_rows, n_cols, run_full_simulation, history):
189
+ from R6_global import sim_state
190
+ from Sim_Engine import initialize_parcels, assign_initial_forage, get_land_forage_rates
191
+
192
+ # sim_state = SimState()
193
+
194
+ if sim_state.parcel_dict is None:
195
+ print("πŸ›  simulate_and_summarize: initializing parcel_dict inside feedback_fcns.py")
196
+ sim_state.parcel_dict = initialize_parcels(parcel_map, cluster_labels)
197
+ assign_initial_forage(sim_state.parcel_dict, get_land_forage_rates())
198
+
199
+ parcel_dict = sim_state.parcel_dict
200
+ round_counter = sim_state.round_counter
201
+
202
+ if parcel_dict is None:
203
+ raise ValueError("❌ parcel_dict is None β€” it must be initialized before calling simulate_and_summarize.")
204
+ strategy_map = {
205
+ "conservative": "conservative",
206
+ "normal": "moderate",
207
+ "aggressive": "aggressive"
208
+ }
209
+ strategy = strategy_map.get(plan_choice.lower())
210
+ # parcel_dict = run_full_simulation(parcel_map, cluster_labels, n_rows, n_cols, strategy=strategy)
211
+
212
+ plot_forage_map_to_file(parcel_dict, n_rows, n_cols, title=f"Round {round_counter} Forage Map")
213
+
214
+ # βœ… ADD THIS BLOCK
215
+ from Sim_Engine import get_health_map, plot_health_map
216
+ health_map = get_health_map(parcel_dict, n_rows, n_cols)
217
+ plot_health_map(health_map, title=f"Round {round_counter} Health Map", save_path="health_map.png")
218
+
219
+
220
+ forage_vals = [p["forage"] for p in parcel_dict.values()]
221
+ avg_forage = sum(forage_vals) / len(forage_vals)
222
+ degraded_pct = 100 * sum(p["degraded"] for p in parcel_dict.values()) / len(parcel_dict)
223
+
224
+ summary = (
225
+ f"After Round {round_counter} with the '{plan_choice}' plan:\n"
226
+ f"Avg forage: {avg_forage:.1f} AUMs, Degraded parcels: {degraded_pct:.1f}%"
227
+ )
228
+ # sim_state.round_counter += 1
229
+ # return summary, "forage_map.png", "health_map.png", sim_state.round_counter
230
+ # return summary, "forage_map.png", "health_map.png", sim_state.round_counter, sim_state.parcel_dict
231
+ from R6_global import sim_state
232
+ sim_state.parcel_dict = parcel_dict # βœ… Save it
233
+
234
+ return summary, "forage_map.png", "health_map.png", sim_state.round_counter, sim_state.parcel_dict
235
+
236
+
237
+
238
+ def full_response(plan_choice, essay_text, current_summary):
239
+ elk_resp = elk_feedback(plan_choice, current_summary)
240
+ usfs_resp = usfs_feedback(plan_choice, essay_text, current_summary)
241
+ return elk_resp, usfs_resp
requirements (11).txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ huggingface_hub==0.25.2
2
+ gradio
3
+ numpy
4
+ pandas
5
+ gradio==4.14.0
6
+ openai>=1.0.0
7
+ scikit-learn
8
+ matplotlib
9
+ numpy
10
+ pillow
11
+ pydantic==2.10.6
12
+ openpyxl
13
+
zone_utils (6).py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cluster_to_class = {
2
+ 0: "desert",
3
+ 1: "pasture",
4
+ 2: "riaprain",
5
+ 3: "sensitive riparian",
6
+ 4: "wetland",
7
+ 5: "water"
8
+ }
9
+
10
+
11
+ import numpy as np
12
+ from collections import deque
13
+
14
+ def identify_zones(parcel_map, connectivity="queen"):
15
+ """
16
+ Identifies contiguous zones in a 2D parcel map using connected component labeling.
17
+
18
+ Parameters:
19
+ parcel_map (np.ndarray): 2D array where each value is a cluster ID (e.g., land type).
20
+ connectivity (str): "rook" (4-way) or "queen" (8-way) connectivity.
21
+
22
+ Returns:
23
+ zone_map (np.ndarray): Same shape as parcel_map, each zone gets a unique integer ID.
24
+ zone_to_cluster (dict): Maps each zone ID to its underlying cluster ID.
25
+ """
26
+ n_rows, n_cols = parcel_map.shape
27
+ zone_map = -1 * np.ones_like(parcel_map, dtype=int)
28
+ visited = np.zeros_like(parcel_map, dtype=bool)
29
+ zone_id = 0
30
+
31
+ if connectivity == "queen":
32
+ directions = [(-1, -1), (-1, 0), (-1, 1),
33
+ (0, -1), (0, 1),
34
+ (1, -1), (1, 0), (1, 1)]
35
+ else: # "rook"
36
+ directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
37
+
38
+ for i in range(n_rows):
39
+ for j in range(n_cols):
40
+ if visited[i, j]:
41
+ continue
42
+ cluster_id = parcel_map[i, j]
43
+ queue = deque([(i, j)])
44
+ while queue:
45
+ x, y = queue.popleft()
46
+ if visited[x, y] or parcel_map[x, y] != cluster_id:
47
+ continue
48
+ visited[x, y] = True
49
+ zone_map[x, y] = zone_id
50
+ for dx, dy in directions:
51
+ nx, ny = x + dx, y + dy
52
+ if 0 <= nx < n_rows and 0 <= ny < n_cols and not visited[nx, ny]:
53
+ if parcel_map[nx, ny] == cluster_id:
54
+ queue.append((nx, ny))
55
+ zone_id += 1
56
+
57
+ # Optional: Map zone_id β†’ cluster_id
58
+ zone_to_cluster = {}
59
+ for zid in range(zone_id):
60
+ indices = np.argwhere(zone_map == zid)
61
+ if len(indices) > 0:
62
+ i, j = indices[0]
63
+ zone_to_cluster[zid] = parcel_map[i, j]
64
+
65
+ return zone_map, zone_to_cluster
66
+
67
+ import matplotlib.pyplot as plt
68
+ import numpy as np
69
+
70
+ def plot_labeled_zones(zone_map, zone_labels, zone_to_cluster, save_path="labeled_zones.png"):
71
+ """
72
+ Plots a zone map with human-readable labels and cluster-based custom colors.
73
+
74
+ Colors (by cluster ID):
75
+ 0: tan
76
+ 1: green
77
+ 2: rose
78
+ 3: red
79
+ 4: purple
80
+ 5: blue
81
+ """
82
+ # Custom color map for cluster IDs (NOT zone IDs)
83
+ cluster_colors = {
84
+ 0: "#D2B48C", # tan
85
+ 1: "#228B22", # green
86
+ 2: "#FF66CC", # rose
87
+ 3: "#FF0000", # red
88
+ 4: "#800080", # purple
89
+ 5: "#1E90FF", # blue
90
+ }
91
+
92
+ n_rows, n_cols = zone_map.shape
93
+ rgb_map = np.zeros((n_rows, n_cols, 3))
94
+
95
+ # Map each parcel to its cluster color
96
+ for i in range(n_rows):
97
+ for j in range(n_cols):
98
+ zone_id = zone_map[i, j]
99
+ cluster_id = zone_to_cluster.get(zone_id, 0)
100
+ hex_color = cluster_colors.get(cluster_id, "#AAAAAA") # fallback = gray
101
+ rgb = tuple(int(hex_color.lstrip('#')[k:k+2], 16)/255 for k in (0, 2, 4))
102
+ rgb_map[i, j] = rgb
103
+
104
+ fig, ax = plt.subplots(figsize=(8, 6))
105
+ ax.imshow(rgb_map)
106
+
107
+ for zone_id, label in zone_labels.items():
108
+ positions = np.argwhere(zone_map == zone_id)
109
+ if len(positions) == 0:
110
+ continue
111
+ center_i, center_j = positions.mean(axis=0)
112
+ cluster = zone_to_cluster.get(zone_id, "?")
113
+ label_text = f"{label} ({cluster})"
114
+ ax.text(center_j, center_i, label_text, color="white", ha="center", va="center",
115
+ fontsize=9, fontweight="bold",
116
+ bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
117
+
118
+ ax.set_title("Labeled Grazing & Riparian Zones (Custom Colors)")
119
+ ax.axis('off')
120
+ plt.tight_layout()
121
+ plt.savefig(save_path)
122
+ plt.close(fig)
123
+
124
+
125
+ import matplotlib.pyplot as plt
126
+ import numpy as np
127
+
128
+ def plot_labeled_zonesold3am(zone_map, zone_labels, zone_to_cluster, save_path="labeled_zones.png"):
129
+ """
130
+ Plots a zone map with fixed colors based on cluster class (e.g., pasture = green, desert = tan),
131
+ and appends the cluster ID to each label (e.g., "A (1)").
132
+
133
+ Parameters:
134
+ zone_map (np.ndarray): 2D array of zone IDs (integers).
135
+ zone_labels (dict): Mapping from zone_id to human-readable label (str).
136
+ zone_to_cluster (dict): Mapping from zone_id to cluster/group ID (int).
137
+ save_path (str): File path to save the plotted image.
138
+ """
139
+
140
+ # === Fixed colors by cluster ID ===
141
+ cluster_color_map = {
142
+ 0: "#d2b48c", # Desert – tan
143
+ 1: "#228B22", # Pasture – green
144
+ 2: "#87CEEB", # Water – light blue
145
+ 3: "#FF69B4", # Riparian – pink
146
+ 4: "#8B0000", # Sensitive riparian – dark red
147
+ 5: "#9370DB", # Town – purple
148
+ }
149
+
150
+ # === Build a color image based on cluster color ===
151
+ rows, cols = zone_map.shape
152
+ color_image = np.zeros((rows, cols, 3))
153
+
154
+ for i in range(rows):
155
+ for j in range(cols):
156
+ zone_id = zone_map[i, j]
157
+ cluster_id = zone_to_cluster.get(zone_id, 0)
158
+ hex_color = cluster_color_map.get(cluster_id, "#888888") # default gray
159
+ rgb = tuple(int(hex_color.lstrip("#")[k:k+2], 16)/255.0 for k in (0, 2, 4))
160
+ color_image[i, j] = rgb
161
+
162
+ # === Plot map ===
163
+ fig, ax = plt.subplots(figsize=(8, 6))
164
+ ax.imshow(color_image)
165
+
166
+ for zone_id in sorted(np.unique(zone_map)):
167
+ if zone_id not in zone_labels:
168
+ continue
169
+ label = zone_labels[zone_id]
170
+ group = zone_to_cluster.get(zone_id, "?")
171
+ label_text = f"{label} ({group})"
172
+ positions = np.argwhere(zone_map == zone_id)
173
+ if len(positions) == 0:
174
+ continue
175
+ center_i, center_j = positions.mean(axis=0)
176
+ ax.text(center_j, center_i, label_text, color="white", ha="center", va="center",
177
+ fontsize=9, fontweight="bold",
178
+ bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
179
+
180
+ ax.set_title("Labeled Grazing & Riparian Zones")
181
+ ax.axis('off')
182
+ plt.tight_layout()
183
+ plt.savefig(save_path)
184
+ plt.close(fig)
185
+
186
+
187
+
188
+ import matplotlib.pyplot as plt
189
+ import numpy as np
190
+
191
+ def plot_labeled_zonesold(zone_map, zone_labels, zone_to_cluster, save_path="labeled_zones.png"):
192
+ """
193
+ Plots a zone map with human-readable labels (e.g., "A", "B", "Riparian A"),
194
+ and appends the cluster ID to each label (e.g., "A (0)").
195
+
196
+ Parameters:
197
+ zone_map (np.ndarray): 2D array of zone IDs (integers).
198
+ zone_labels (dict): Mapping from zone_id to human-readable label (str).
199
+ zone_to_cluster (dict): Mapping from zone_id to cluster/group ID (int).
200
+ save_path (str): File path to save the plotted image.
201
+ """
202
+ unique_ids = sorted(np.unique(zone_map))
203
+ color_map = plt.get_cmap('tab20', len(unique_ids))
204
+
205
+ fig, ax = plt.subplots(figsize=(8, 6))
206
+ cax = ax.imshow(zone_map, cmap=color_map, vmin=0, vmax=len(unique_ids) - 1)
207
+
208
+ # Add label with group ID
209
+ for zone_id in unique_ids:
210
+ if zone_id not in zone_labels:
211
+ continue
212
+ label = zone_labels[zone_id]
213
+ group = zone_to_cluster.get(zone_id, "?")
214
+ label_text = f"{label} ({group})" # append group ID
215
+
216
+ positions = np.argwhere(zone_map == zone_id)
217
+ if len(positions) == 0:
218
+ continue
219
+ center_i, center_j = positions.mean(axis=0)
220
+ ax.text(center_j, center_i, label_text, color="white", ha="center", va="center",
221
+ fontsize=9, fontweight="bold",
222
+ bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
223
+
224
+ ax.set_title("Labeled Grazing & Riparian Zones")
225
+ ax.axis('off')
226
+ plt.tight_layout()
227
+ plt.savefig(save_path)
228
+ plt.close(fig)
229
+
230
+
231
+ def assign_zone_labels(zone_to_cluster):
232
+ """
233
+ Assigns human-readable labels to each zone based on its cluster type.
234
+ Riparian zones are labeled like 'Riparian A', 'Riparian B', etc.
235
+ Other zones are labeled 'A', 'B', etc. by group type.
236
+
237
+ Returns:
238
+ zone_labels (dict): zone_id β†’ label string
239
+ """
240
+ label_counts = {} # track how many zones per type
241
+ zone_labels = {}
242
+
243
+ for zone_id, cluster_id in zone_to_cluster.items():
244
+ land_class = cluster_to_class.get(cluster_id, f"cluster{cluster_id}")
245
+ count = label_counts.get(land_class, 0)
246
+ suffix = chr(65 + count) # A, B, C...
247
+ if "riparian" in land_class:
248
+ label = f"{land_class.title()} {suffix}"
249
+ else:
250
+ label = f"{suffix}"
251
+ zone_labels[zone_id] = label
252
+ label_counts[land_class] = count + 1
253
+
254
+ return zone_labels
255
+
256
+
257
+ def assign_zone_labels_old(zone_to_cluster, cluster_to_class):
258
+ """
259
+ Creates a dictionary mapping zone_id β†’ human-readable labels (e.g., "A", "Riparian B").
260
+
261
+ Parameters:
262
+ zone_to_cluster (dict): zone_id β†’ cluster ID
263
+ cluster_to_class (dict): cluster ID β†’ class name (e.g., "pasture", "riparian")
264
+
265
+ Returns:
266
+ dict: zone_id β†’ human-friendly label
267
+ """
268
+ label_counts = {} # Track how many zones per class
269
+ zone_labels = {}
270
+
271
+ for zone_id, cluster_id in zone_to_cluster.items():
272
+ zone_class = cluster_to_class.get(cluster_id, "Unknown")
273
+ count = label_counts.get(zone_class, 0)
274
+
275
+ # Generate a label like "Riparian A", "Pasture B", etc.
276
+ letter = chr(ord('A') + count)
277
+ label = f"{zone_class.capitalize()} {letter}" if zone_class != "Unknown" else f"Zone {zone_id}"
278
+
279
+ zone_labels[zone_id] = label
280
+ label_counts[zone_class] = count + 1
281
+
282
+ return zone_labels
283
+
284
+ import numpy as np
285
+ import pandas as pd
286
+
287
+ def save_zone_info_to_excel(parcel_map, zone_map, zone_labels, zone_to_cluster, cluster_to_class, save_path="zone_details.xlsx"):
288
+ """
289
+ Save detailed zone information to an Excel file.
290
+
291
+ Parameters:
292
+ parcel_map (np.ndarray): 2D array with cluster IDs.
293
+ zone_map (np.ndarray): 2D array with zone IDs.
294
+ zone_labels (dict): zone_id β†’ human-friendly label (e.g., "A", "Riparian B")
295
+ zone_to_cluster (dict): zone_id β†’ cluster ID
296
+ cluster_to_class (dict): cluster ID β†’ land class string (e.g., "pasture", "riparian")
297
+ save_path (str): File path to save Excel.
298
+ """
299
+ rows, cols = parcel_map.shape
300
+ data = []
301
+
302
+ for i in range(rows):
303
+ for j in range(cols):
304
+ zone_id = zone_map[i, j]
305
+ cluster_id = parcel_map[i, j]
306
+ label = zone_labels.get(zone_id, "Unknown")
307
+ zone_class = cluster_to_class.get(cluster_id, "Unknown")
308
+ data.append({
309
+ "Row": i,
310
+ "Col": j,
311
+ "Zone ID": zone_id,
312
+ "Zone Label": label,
313
+ "Cluster ID": cluster_id,
314
+ "Zone Class": zone_class
315
+ })
316
+
317
+ df = pd.DataFrame(data)
318
+ df.to_excel(save_path, index=False)
319
+ print(f"βœ… Zone info saved to {save_path}")
320
+
321
+
322
+ def override_zone_id_and_label(zone_map, zone_labels, from_id, from_label, to_id, to_label):
323
+ """
324
+ Reassigns all parcels with a given zone ID and label to a new zone ID and label.
325
+
326
+ Parameters:
327
+ zone_map (np.ndarray): 2D array with zone IDs.
328
+ zone_labels (dict): zone_id β†’ label.
329
+ from_id (int): Zone ID to search for.
330
+ from_label (str): Must match the current label for that zone.
331
+ to_id (int): Zone ID to assign.
332
+ to_label (str): New label for the new zone ID.
333
+
334
+ Returns:
335
+ zone_map (np.ndarray): Updated zone map.
336
+ zone_labels (dict): Updated labels.
337
+ """
338
+ # Step 1: Confirm the label matches
339
+ if zone_labels.get(from_id) != from_label:
340
+ print(f"❌ Mismatch: Zone {from_id} label is '{zone_labels.get(from_id)}', not '{from_label}'")
341
+ return zone_map, zone_labels
342
+
343
+ # Step 2: Loop through all parcels
344
+ for i in range(zone_map.shape[0]):
345
+ for j in range(zone_map.shape[1]):
346
+ if zone_map[i, j] == from_id:
347
+ zone_map[i, j] = to_id # Override ID
348
+
349
+ # Step 3: Update label dictionary
350
+ zone_labels[to_id] = to_label
351
+ if from_id not in zone_map:
352
+ zone_labels.pop(from_id, None)
353
+
354
+ print(f"βœ… Overrode zone {from_id} ('{from_label}') β†’ zone {to_id} ('{to_label}')")
355
+ return zone_map, zone_labels
356
+
357
+
358
+
359
+ def remap_zone_id_and_label(zone_map, zone_labels, old_zone_id, old_label, new_zone_id, new_label):
360
+ """
361
+ For all parcels where zone_id == old_zone_id and label == old_label:
362
+ β†’ Change zone_id to new_zone_id and label to new_label.
363
+
364
+ Args:
365
+ zone_map (np.ndarray): 2D map of zone IDs.
366
+ zone_labels (dict): zone_id β†’ label.
367
+ old_zone_id (int)
368
+ old_label (str)
369
+ new_zone_id (int)
370
+ new_label (str)
371
+
372
+ Returns:
373
+ zone_map, zone_labels
374
+ """
375
+ # Only proceed if the label for old_zone_id matches
376
+ if zone_labels.get(old_zone_id) != old_label:
377
+ print(f"❌ Skipping: Label mismatch for zone {old_zone_id}")
378
+ return zone_map, zone_labels
379
+
380
+ # Go through every (i,j) and remap matching zones
381
+ rows, cols = zone_map.shape
382
+ for i in range(rows):
383
+ for j in range(cols):
384
+ if zone_map[i, j] == old_zone_id:
385
+ zone_map[i, j] = new_zone_id
386
+
387
+ # Update the label dictionary
388
+ zone_labels[new_zone_id] = new_label
389
+ if old_zone_id in zone_labels:
390
+ del zone_labels[old_zone_id]
391
+
392
+ print(f"βœ… Reassigned zone {old_zone_id} ('{old_label}') β†’ {new_zone_id} ('{new_label}')")
393
+ return zone_map, zone_labels
394
+
395
+
396
+ import matplotlib.pyplot as plt
397
+ import numpy as np
398
+
399
+ def plot_labeled_zones_old(zone_map, zone_labels, save_path="labeled_zones.png"):
400
+ """
401
+ Plots a zone map with human-readable labels (e.g., "A", "B", "Riparian A").
402
+
403
+ Parameters:
404
+ zone_map (np.ndarray): 2D array of integers where each unique int is a zone ID.
405
+ zone_labels (dict): Mapping from zone_id (int) to human label (str).
406
+ save_path (str): File path to save the plotted image.
407
+ """
408
+ unique_ids = sorted(np.unique(zone_map))
409
+ color_map = plt.get_cmap('tab20', len(unique_ids))
410
+
411
+ fig, ax = plt.subplots(figsize=(8, 6))
412
+ cax = ax.imshow(zone_map, cmap=color_map, vmin=0, vmax=len(unique_ids) - 1)
413
+
414
+ # Add human-readable labels at zone centers
415
+ for zone_id in unique_ids:
416
+ if zone_id not in zone_labels:
417
+ continue
418
+ label = zone_labels[zone_id]
419
+ positions = np.argwhere(zone_map == zone_id)
420
+ if len(positions) == 0:
421
+ continue
422
+ center_i, center_j = positions.mean(axis=0)
423
+ ax.text(center_j, center_i, label, color="white", ha="center", va="center",
424
+ fontsize=9, fontweight="bold", bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
425
+
426
+ ax.set_title("Labeled Grazing & Riparian Zones")
427
+ ax.axis('off')
428
+ plt.tight_layout()
429
+ plt.savefig(save_path)
430
+ plt.close(fig)
431
+
432
+