jeffrey1963 commited on
Commit
6fde63f
·
verified ·
1 Parent(s): d32a150

Upload 3 files

Browse files
Files changed (3) hide show
  1. Sim_Engine (7).py +207 -0
  2. app (21).py +122 -0
  3. feedback_fcns (7).py +167 -0
Sim_Engine (7).py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def initialize_parcels(parcel_map, cluster_labels):
2
+ parcel_dict = {}
3
+ for i in range(parcel_map.shape[0]):
4
+ for j in range(parcel_map.shape[1]):
5
+ cluster_id = parcel_map[i, j]
6
+ land_type = cluster_labels.get(cluster_id, "Unknown")
7
+
8
+ parcel_dict[(i, j)] = {
9
+ "land_type": land_type,
10
+ "forage": None,
11
+ "health": 1.0,
12
+ "degraded": False,
13
+ "cattle_grazing": {
14
+ "conservative": land_type == "Productive Grass",
15
+ "moderate": land_type in ["Productive Grass", "Pasture/Desert"],
16
+ "aggressive": land_type not in ["Water"]
17
+ },
18
+ "elk_grazing": {
19
+ "default": land_type in ["Riparian Sensitive Zone", "Productive Grass"]
20
+ }
21
+ }
22
+ return parcel_dict
23
+
24
+ def get_land_forage_rates():
25
+ return {
26
+ 'Productive Grass': 1.0,
27
+ 'Pasture/Desert': 0.4,
28
+ 'Riparian Sensitive Zone': 1.2,
29
+ 'Rocky Area': 0.2,
30
+ 'Water': 0.0
31
+ }
32
+
33
+
34
+ def assign_initial_forage(parcel_dict, land_forage_rates):
35
+ for parcel in parcel_dict.values():
36
+ rate = land_forage_rates.get(parcel["land_type"], 0.0)
37
+ parcel["forage"] = rate * 100 # initial AUMs
38
+
39
+ import numpy as np
40
+ import numpy as np
41
+ import matplotlib.pyplot as plt
42
+ from collections import Counter
43
+ from matplotlib.colors import ListedColormap
44
+ import matplotlib.patches as mpatches
45
+
46
+ def simulate_period(parcel_dict, grazing_strategy="moderate", cattle_stocking_rate=100000, elk_pressure=3000):
47
+ print(f"\n🟢 Running LP-based simulation using grazing strategy: **{grazing_strategy.upper()}**")
48
+
49
+ import numpy as np
50
+ from scipy.optimize import linprog
51
+
52
+ keys = list(parcel_dict.keys())
53
+ n_rows = max(i for i, _ in keys) + 1
54
+ n_cols = max(j for _, j in keys) + 1
55
+ num_cells = n_rows * n_cols
56
+
57
+ # Step 1: Regrowth
58
+ land_growth_rates = {
59
+ 'Productive Grass': 1.0,
60
+ 'Pasture/Desert': 0.4,
61
+ 'Riparian Sensitive Zone': 1.2,
62
+ 'Rocky Area': 0.2,
63
+ 'Water': 0.0
64
+ }
65
+ for parcel in parcel_dict.values():
66
+ base_growth = land_growth_rates.get(parcel["land_type"], 0.0)
67
+ weather = np.random.normal(1.0, 0.15)
68
+ regrowth = base_growth * weather * 1
69
+ regrowth *= parcel["health"]
70
+ parcel["forage"] = min(parcel["forage"] + regrowth, 100)
71
+
72
+ # Step 2: Subtract uniform elk grazing from all parcels
73
+ elk_grazing_per_parcel = elk_pressure / num_cells
74
+ for parcel in parcel_dict.values():
75
+ parcel["forage"] -= elk_grazing_per_parcel
76
+ parcel["forage"] = max(parcel["forage"], 0.0)
77
+
78
+ # Step 3: LP for cattle
79
+ cost = []
80
+ bounds = []
81
+ eligible_keys = []
82
+ for i in range(n_rows):
83
+ for j in range(n_cols):
84
+ p = parcel_dict[(i, j)]
85
+ if not p["cattle_grazing"].get(grazing_strategy, False):
86
+ cost.append(0)
87
+ bounds.append((0, 0))
88
+ continue
89
+ if grazing_strategy in {"conservative", "moderate"} and p["land_type"] == "Riparian Sensitive Zone":
90
+ cost.append(0)
91
+ bounds.append((0, 0))
92
+ continue
93
+ cost.append((i + j) * 0.02)
94
+ bounds.append((0, p["forage"]))
95
+ eligible_keys.append((i, j))
96
+
97
+ A_eq = [1.0 if b[1] > 0 else 0.0 for b in bounds]
98
+ b_eq = [cattle_stocking_rate]
99
+
100
+ result = linprog(c=cost, A_eq=[A_eq], b_eq=b_eq, bounds=bounds, method="highs")
101
+ if not result.success:
102
+ raise RuntimeError("Grazing LP failed: " + result.message)
103
+
104
+ grazing_values = result.x
105
+
106
+ # Step 4: Apply grazing and your specified health rule
107
+ for idx, ((i, j), x) in enumerate(zip(parcel_dict.keys(), grazing_values)):
108
+ parcel = parcel_dict[(i, j)]
109
+ parcel["forage"] -= x
110
+
111
+ # ✅ Your health rule (fully time-dynamic)
112
+ if parcel["forage"] <= 0:
113
+ parcel["health"] = max(parcel["health"] - 0.25, 0.0)
114
+ elif parcel["forage"] < 20:
115
+ parcel["health"] = max(parcel["health"] - 0.1, 0.0)
116
+ else:
117
+ parcel["health"] = min(parcel["health"] + 0.02, 1.0)
118
+
119
+ def simulate_periodold(parcel_dict, grazing_strategy="moderate", cattle_stocking_rate=5000, elk_pressure=3000):
120
+ print(f"\n🟢 Running simulation using cattle grazing strategy: **{grazing_strategy.upper()}**")
121
+
122
+ land_growth_rates = {
123
+ 'Productive Grass': 1.0,
124
+ 'Pasture/Desert': 0.4,
125
+ 'Riparian Sensitive Zone': 1.2,
126
+ 'Rocky Area': 0.2,
127
+ 'Water': 0.0
128
+ }
129
+
130
+ # 1. Simulate forage regrowth for 8 months
131
+ for parcel in parcel_dict.values():
132
+ base_growth = land_growth_rates.get(parcel["land_type"], 0.0)
133
+ weather = np.random.normal(1.0, 0.15)
134
+ regrowth = base_growth * weather * 8 # ← 8 months, as you said
135
+ regrowth *= parcel["health"] # degrade means slower regrowth
136
+ parcel["forage"] = min(parcel["forage"] + regrowth, 100)
137
+
138
+ # 2. Count eligible parcels
139
+ total_grazed_parcels = sum(
140
+ 1 for parcel in parcel_dict.values() if parcel["cattle_grazing"].get(grazing_strategy, False)
141
+ )
142
+ if total_grazed_parcels == 0:
143
+ print("⚠️ No parcels match the selected grazing strategy.")
144
+ return
145
+
146
+ cattle_grazing_per_parcel = cattle_stocking_rate / total_grazed_parcels
147
+ elk_grazing_per_parcel = elk_pressure / len(parcel_dict)
148
+
149
+ # 3. Simulate grazing and degradation
150
+ for parcel in parcel_dict.values():
151
+ if not parcel["cattle_grazing"].get(grazing_strategy, False):
152
+ continue
153
+
154
+ total_grazing = cattle_grazing_per_parcel + elk_grazing_per_parcel
155
+
156
+ if total_grazing > parcel["forage"]:
157
+ parcel["degraded"] = True
158
+ parcel["health"] = max(parcel["health"] - 0.1, 0.0)
159
+ else:
160
+ parcel["health"] = min(parcel["health"] + 0.02, 1.0)
161
+
162
+ parcel["forage"] = max(parcel["forage"] - total_grazing, 0)
163
+
164
+ def get_forage_map(parcel_dict, n_rows, n_cols):
165
+ return np.array([[parcel_dict[(i, j)]["forage"] for j in range(n_cols)] for i in range(n_rows)])
166
+
167
+ def get_health_map(parcel_dict, n_rows, n_cols):
168
+ """
169
+ Returns a 2D numpy array representing the health of each parcel.
170
+ """
171
+ return np.array([[parcel_dict[(i, j)]["health"] for j in range(n_cols)] for i in range(n_rows)])
172
+
173
+
174
+ def plot_health_map(health_map, title="Parcel Health Levels", save_path=None):
175
+ """
176
+ Plots a heatmap of the parcel health values.
177
+ """
178
+ import matplotlib.pyplot as plt
179
+
180
+ plt.figure(figsize=(8, 6))
181
+ plt.imshow(health_map, cmap='RdYlGn', origin='upper', vmin=0, vmax=1)
182
+ plt.colorbar(label="Health Index (0–1)")
183
+ plt.title(title)
184
+ plt.axis('off')
185
+ plt.tight_layout()
186
+ if save_path:
187
+ plt.savefig(save_path)
188
+ plt.show()
189
+
190
+
191
+
192
+ def plot_forage_map(forage_map, title="Parcel Forage Levels"):
193
+ plt.figure(figsize=(8, 6))
194
+ plt.imshow(forage_map, cmap='YlGn', origin='upper')
195
+ plt.colorbar(label="Forage AUMs")
196
+ plt.title(title)
197
+ plt.axis('off')
198
+ plt.tight_layout()
199
+ plt.show()
200
+
201
+ def run_full_simulation(parcel_map, cluster_labels, n_rows, n_cols, strategy="moderate"):
202
+ parcel_dict = initialize_parcels(parcel_map, cluster_labels)
203
+ land_forage_rates = get_land_forage_rates()
204
+ assign_initial_forage(parcel_dict, land_forage_rates)
205
+ simulate_period(parcel_dict, grazing_strategy=strategy)
206
+ return parcel_dict
207
+
app (21).py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
20
+ # === Setup on Launch ===
21
+ img = load_and_crop_image("Carson_map.png")
22
+ clustered_img = cluster_image(img)
23
+ parcel_map, n_rows, n_cols = build_parcel_map(clustered_img)
24
+ cluster_labels = get_cluster_labels()
25
+ land_colors = get_land_colors()
26
+ plot_parcel_map_to_file(parcel_map, cluster_labels, land_colors, save_path="clustered_map.png")
27
+
28
+ # === Zoning ===
29
+
30
+ # 1. Identify contiguous zones
31
+ zone_map, zone_to_cluster = identify_zones(parcel_map, connectivity="queen")
32
+
33
+ # 2. Assign human-readable labels (before override)
34
+ zone_labels = assign_zone_labels(zone_to_cluster)
35
+ # === Manual override for mislabeled riparian zone ===
36
+ # First, update the zone label once
37
+ for zid, lbl in zone_labels.items():
38
+ if lbl == "A" and zone_to_cluster[zid] == 1:
39
+ zone_labels[zid] = "Riparian A1"
40
+ if lbl == "M" and zone_to_cluster[zid] == 1:
41
+ zone_labels[zid] = "Riparian A2"
42
+ # Then update all matching parcels
43
+ for i in range(n_rows):
44
+ for j in range(n_cols):
45
+ zone_id = zone_map[i, j]
46
+ if zone_labels.get(zone_id) == "Riparian A1":
47
+ parcel_map[i, j] = 2
48
+ if zone_labels.get(zone_id) == "Riparian A2":
49
+ parcel_map[i, j] = 2
50
+
51
+ #
52
+
53
+
54
+ # ⬇️ Add this block right after the override
55
+ zone_to_cluster = {}
56
+ for zone_id in np.unique(zone_map):
57
+ indices = np.argwhere(zone_map == zone_id)
58
+ if len(indices) > 0:
59
+ i, j = indices[0]
60
+ zone_to_cluster[zone_id] = parcel_map[i, j]
61
+
62
+ # 6. Plot labeled zones after override and mapping
63
+ plot_labeled_zones(zone_map, zone_labels, zone_to_cluster, save_path="zones_labeled.png")
64
+
65
+ # 5. Define cluster-to-class mapping (should stay after override)
66
+ cluster_to_class = {
67
+ 0: "desert",
68
+ 1: "pasture",
69
+ 2: "riparain",
70
+ 3: "sensitive riparian",
71
+ 4: "wetland",
72
+ 5: "water"
73
+ }
74
+
75
+ # 7. Save zone info to Excel
76
+ zone_excel_path = "zone_info.xlsx"
77
+ save_zone_info_to_excel(
78
+ parcel_map, zone_map, zone_labels, zone_to_cluster, cluster_to_class,
79
+ save_path=zone_excel_path
80
+ )
81
+
82
+
83
+ client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
84
+
85
+ # === Gradio App ===
86
+ with gr.Blocks() as demo:
87
+ gr.Markdown("# AGEC 3052 — Grazing Strategy Simulation")
88
+ gr.Image(value="clustered_map.png", label="Initial 25×25 Parcel Layout")
89
+ gr.Image(value="zones_labeled.png", label="Labeled Pasture & Riparian Zones")
90
+ # ✅ Downloadable Excel
91
+ gr.File(value=zone_excel_path, label="Download Zone Info (Excel)")
92
+
93
+ plan = gr.Radio(["Conservative", "Normal", "Aggressive"], label="Grazing Plan")
94
+ essay = gr.Textbox(lines=8, label="Your Essay Justifying the Plan")
95
+
96
+ elk_output = gr.Textbox(label="Elk Stakeholder Feedback")
97
+ usfs_output = gr.Textbox(label="USFS Feedback")
98
+ sim_output = gr.Textbox(label="Simulation Results", lines=2)
99
+ sim_image = gr.Image(label="Forage Map After Simulation", type="filepath")
100
+ health_image = gr.Image(label="Health Map After Simulation", type="filepath")
101
+
102
+ round_counter = gr.State(value=1)
103
+ history = gr.State(value=[summarize_initial_conditions(n_rows, n_cols)])
104
+
105
+ def submit_handler(plan_choice, essay_text, history_val):
106
+ return full_response(plan_choice, essay_text, history_val[-1])
107
+
108
+ def sim_handler(plan_choice, round_val, history_val):
109
+ summary, map_path, health_path, new_round = simulate_and_summarize(
110
+ plan_choice, round_val, parcel_map, cluster_labels, n_rows, n_cols,
111
+ run_full_simulation, history_val
112
+ )
113
+ history_val.append(summary)
114
+ return summary, map_path, health_path, new_round, history_val
115
+
116
+ submit_btn = gr.Button("Submit Grazing Plan")
117
+ sim_btn = gr.Button("Run Simulation")
118
+
119
+ submit_btn.click(fn=submit_handler, inputs=[plan, essay, history], outputs=[elk_output, usfs_output])
120
+ sim_btn.click(fn=sim_handler, inputs=[plan, round_counter, history], outputs=[sim_output, sim_image, health_image, round_counter, history])
121
+
122
+ demo.launch()
feedback_fcns (7).py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ num_parcels = n_rows * n_cols
12
+ avg_forage = round(random.uniform(6.5, 8.0), 1)
13
+ degraded_pct = round(random.uniform(0.0, 2.0), 1)
14
+ riparian_health = random.choice(["excellent", "moderate", "fragile"])
15
+ elk_corridor_status = random.choice([
16
+ "completely intact and lightly used",
17
+ "intact but under slight pressure from cattle movement",
18
+ "showing signs of fragmentation near key crossings"
19
+ ])
20
+ rainfall_outlook = random.choice(["normal", "below average", "above average"])
21
+
22
+ return (
23
+ f"There are {num_parcels} parcels in total. Grazing has not yet occurred.\n"
24
+ f"Average forage availability is {avg_forage} AUMs per parcel, with about {degraded_pct}% of land already degraded due to prior conditions.\n"
25
+ f"Riparian zone condition is {riparian_health}, and the elk movement corridor is {elk_corridor_status}.\n"
26
+ f"The seasonal rainfall outlook is {rainfall_outlook}."
27
+ )
28
+
29
+ def plot_forage_map_to_file(parcel_dict, n_rows, n_cols, title="Forage Map", save_path="forage_map.png"):
30
+ forage_map = np.array([
31
+ [parcel_dict[(i, j)]["forage"] for j in range(n_cols)]
32
+ for i in range(n_rows)
33
+ ])
34
+ fig, ax = plt.subplots(figsize=(8, 6))
35
+ cax = ax.imshow(forage_map, cmap='YlGn', origin='upper')
36
+ fig.colorbar(cax, label="Forage AUMs")
37
+ ax.set_title(title)
38
+ ax.axis('off')
39
+ plt.tight_layout()
40
+ plt.savefig(save_path)
41
+ plt.close(fig)
42
+
43
+ def elk_feedback(plan_choice, current_summary):
44
+ prompt = f"""
45
+ You represent a coalition of elk-related interests: conservationists, hunting advocates, and the hospitality/lodging industry.
46
+ A student has selected the **'{plan_choice}'** cattle grazing strategy. Below are the current ecological conditions:
47
+ -----
48
+ {current_summary}
49
+ -----
50
+ Please do the following:
51
+ - Explicitly choose **one elk management strategy** from the list:
52
+ - **Preserve**: strict elk protections, no hunting, unrestricted movement
53
+ - **Cooperate**: shared use corridor, some riparian restrictions, sustainable elk population
54
+ - **Exploit**: prioritize hunting/tourism, tolerate reduced elk numbers and access
55
+ - Reflect each group's view briefly, but unify the final position.
56
+ - Justify your strategy choice based on the above ecological indicators.
57
+ """
58
+ response = client.chat.completions.create(
59
+ model="gpt-3.5-turbo",
60
+ messages=[{"role": "user", "content": prompt}],
61
+ temperature=0.8
62
+ )
63
+ return response.choices[0].message.content
64
+
65
+ def usfs_feedback(plan_choice, student_essay, current_summary):
66
+ # Extract the AUM line from the summary
67
+ aum_line = ""
68
+ for line in current_summary.split("\n"):
69
+ if "Average forage availability" in line:
70
+ aum_line = line.strip()
71
+ break
72
+
73
+ prompt = f"""
74
+ You are a USFS land management agent evaluating a student’s cattle grazing proposal.
75
+
76
+ The student selected the **'{plan_choice}'** strategy and submitted this justification:
77
+ -----
78
+ {student_essay}
79
+ -----
80
+
81
+ Here are the current rangeland conditions:
82
+ -----
83
+ {current_summary}
84
+ -----
85
+
86
+ 🚨 **Must Include Block**:
87
+ You must include this sentence exactly in your response:
88
+ → "{aum_line}"
89
+
90
+ Then provide your evaluation:
91
+ - Comment on forage, degradation, riparian and corridor health
92
+ - State whether the plan is ecologically sound
93
+ - Suggest improvements if needed
94
+ - Keep the tone professional, clear, and grounded in the data
95
+ """
96
+
97
+ response = client.chat.completions.create(
98
+ model="gpt-3.5-turbo",
99
+ messages=[{"role": "user", "content": prompt}],
100
+ temperature=0.7
101
+ )
102
+ return response.choices[0].message.content
103
+
104
+ def simulate_and_summarizeold(plan_choice, round_counter, parcel_dict, parcel_map, cluster_labels, n_rows, n_cols, elk_pressure):
105
+ # Interpret strategy
106
+ strategy_map = {
107
+ "conservative": "conservative",
108
+ "normal": "moderate",
109
+ "aggressive": "aggressive"
110
+ }
111
+ strategy = strategy_map.get(plan_choice.lower(), "moderate")
112
+
113
+ # ✅ Reuse incoming parcel_dict — do not reset
114
+ simulate_period(parcel_dict, grazing_strategy=strategy, elk_pressure=elk_pressure)
115
+
116
+ # Extract summary
117
+ forage_vals = [p["forage"] for p in parcel_dict.values()]
118
+ avg_forage = sum(forage_vals) / len(forage_vals)
119
+ degraded_pct = 100 * sum(p["health"] < 0.5 for p in parcel_dict.values()) / len(parcel_dict)
120
+
121
+ summary = (
122
+ f"After Round {round_counter} with the '{plan_choice}' plan:\n"
123
+ f"Avg forage: {avg_forage:.1f} AUMs, Parcels with low health (<0.5): {degraded_pct:.1f}%"
124
+ )
125
+
126
+ # Generate map visuals
127
+ from Sim_Engine import get_forage_map, get_health_map, plot_forage_map, plot_health_map
128
+ forage_map = get_forage_map(parcel_dict, n_rows, n_cols)
129
+ health_map = get_health_map(parcel_dict, n_rows, n_cols)
130
+ plot_forage_map(forage_map, title=f"Forage Map (Round {round_counter})", save_path="forage_map.png")
131
+ plot_health_map(health_map, title=f"Health Map (Round {round_counter})", save_path="health_map.png")
132
+
133
+ return summary, "forage_map.png", "health_map.png", round_counter + 1, parcel_dict
134
+
135
+ def simulate_and_summarize(plan_choice, round_counter, parcel_map, cluster_labels, n_rows, n_cols, run_full_simulation, history):
136
+
137
+ strategy_map = {
138
+ "conservative": "conservative",
139
+ "normal": "moderate",
140
+ "aggressive": "aggressive"
141
+ }
142
+ strategy = strategy_map.get(plan_choice.lower())
143
+ parcel_dict = run_full_simulation(parcel_map, cluster_labels, n_rows, n_cols, strategy=strategy)
144
+
145
+ plot_forage_map_to_file(parcel_dict, n_rows, n_cols, title=f"Round {round_counter} Forage Map")
146
+
147
+ # ✅ ADD THIS BLOCK
148
+ from Sim_Engine import get_health_map, plot_health_map
149
+ health_map = get_health_map(parcel_dict, n_rows, n_cols)
150
+ plot_health_map(health_map, title=f"Round {round_counter} Health Map", save_path="health_map.png")
151
+
152
+
153
+ forage_vals = [p["forage"] for p in parcel_dict.values()]
154
+ avg_forage = sum(forage_vals) / len(forage_vals)
155
+ degraded_pct = 100 * sum(p["degraded"] for p in parcel_dict.values()) / len(parcel_dict)
156
+
157
+ summary = (
158
+ f"After Round {round_counter} with the '{plan_choice}' plan:\n"
159
+ f"Avg forage: {avg_forage:.1f} AUMs, Degraded parcels: {degraded_pct:.1f}%"
160
+ )
161
+ return summary, "forage_map.png", "health_map.png", round_counter + 1
162
+
163
+
164
+ def full_response(plan_choice, essay_text, current_summary):
165
+ elk_resp = elk_feedback(plan_choice, current_summary)
166
+ usfs_resp = usfs_feedback(plan_choice, essay_text, current_summary)
167
+ return elk_resp, usfs_resp