1.3.64
 
Loading...
Searching...
No Matches
selfTest.cpp
1#include "PlantArchitecture.h"
2
3#define DOCTEST_CONFIG_IMPLEMENT
4#include <doctest.h>
5#include "doctest_utils.h"
6#include "global.h"
7
8using namespace helios;
9
10double err_tol = 1e-7;
11
12DOCTEST_TEST_CASE("PlantArchitecture Constructor") {
13 Context context;
14 DOCTEST_CHECK_NOTHROW(PlantArchitecture pa_test(&context));
15}
16
17DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes valid input") {
18 ShootParameters sp_test;
19 std::vector<std::string> labels = {"typeA", "typeB"};
20 std::vector<float> probabilities = {0.4f, 0.6f};
21 DOCTEST_CHECK_NOTHROW(sp_test.defineChildShootTypes(labels, probabilities));
22}
23
24DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes size mismatch") {
25 capture_cerr cerr_buffer;
26 ShootParameters sp_test;
27 std::vector<std::string> labels = {"typeA", "typeB"};
28 std::vector<float> probabilities = {0.4f};
29 DOCTEST_CHECK_THROWS(sp_test.defineChildShootTypes(labels, probabilities));
30}
31
32DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes empty vectors") {
33 capture_cerr cerr_buffer;
34 ShootParameters sp_test;
35 std::vector<std::string> labels = {};
36 std::vector<float> probabilities = {};
37 DOCTEST_CHECK_THROWS(sp_test.defineChildShootTypes(labels, probabilities));
38}
39
40DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes probabilities sum not equal to 1") {
41 capture_cerr cerr_buffer;
42 ShootParameters sp_test;
43 std::vector<std::string> labels = {"typeA", "typeB"};
44 std::vector<float> probabilities = {0.3f, 0.6f}; // Sums to 0.9
45 DOCTEST_CHECK_THROWS(sp_test.defineChildShootTypes(labels, probabilities));
46}
47
48DOCTEST_TEST_CASE("PlantArchitecture defineShootType") {
49 Context context;
50 PlantArchitecture pa_test(&context);
51 ShootParameters sp_define;
52 DOCTEST_CHECK_NOTHROW(pa_test.defineShootType("newShootType", sp_define));
53}
54
55DOCTEST_TEST_CASE("LeafPrototype Constructor") {
56 Context context;
57 std::minstd_rand0 *generator = context.getRandomGenerator();
58 LeafPrototype lp_test(generator);
59 DOCTEST_CHECK(lp_test.subdivisions == 1);
60 DOCTEST_CHECK(lp_test.unique_prototypes == 1);
61 DOCTEST_CHECK(lp_test.leaf_offset.x == doctest::Approx(0.0f).epsilon(err_tol));
62 DOCTEST_CHECK(lp_test.leaf_offset.y == doctest::Approx(0.0f).epsilon(err_tol));
63 DOCTEST_CHECK(lp_test.leaf_offset.z == doctest::Approx(0.0f).epsilon(err_tol));
64}
65
66DOCTEST_TEST_CASE("PhytomerParameters Constructor") {
67 Context context;
68 std::minstd_rand0 *generator = context.getRandomGenerator();
69 DOCTEST_CHECK_NOTHROW(PhytomerParameters pp_test(generator));
70}
71
72DOCTEST_TEST_CASE("Plant Library Model Building - almond") {
73 Context context;
74 PlantArchitecture plantarchitecture(&context);
75 plantarchitecture.disableMessages();
76 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("almond"));
77 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
78}
79
80DOCTEST_TEST_CASE("Plant Library Model Building - apple") {
81 Context context;
82 PlantArchitecture plantarchitecture(&context);
83 plantarchitecture.disableMessages();
84 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("apple"));
85 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
86}
87
88DOCTEST_TEST_CASE("Plant Library Model Building - asparagus") {
89 Context context;
90 PlantArchitecture plantarchitecture(&context);
91 plantarchitecture.disableMessages();
92 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("asparagus"));
93 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
94}
95
96DOCTEST_TEST_CASE("Plant Library Model Building - bindweed") {
97 Context context;
98 PlantArchitecture plantarchitecture(&context);
99 plantarchitecture.disableMessages();
100 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bindweed"));
101 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
102}
103
104DOCTEST_TEST_CASE("Plant Library Model Building - bean") {
105 Context context;
106 PlantArchitecture plantarchitecture(&context);
107 plantarchitecture.disableMessages();
108 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
109 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
110}
111
112DOCTEST_TEST_CASE("Plant Library Model Building - cheeseweed") {
113 Context context;
114 PlantArchitecture plantarchitecture(&context);
115 plantarchitecture.disableMessages();
116 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("cheeseweed"));
117 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
118}
119
120DOCTEST_TEST_CASE("Plant Library Model Building - cowpea") {
121 Context context;
122 PlantArchitecture plantarchitecture(&context);
123 plantarchitecture.disableMessages();
124 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("cowpea"));
125 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
126}
127
128DOCTEST_TEST_CASE("Plant Library Model Building - grapevine_VSP") {
129 Context context;
130 PlantArchitecture plantarchitecture(&context);
131 plantarchitecture.disableMessages();
132 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("grapevine_VSP"));
133 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
134}
135
136DOCTEST_TEST_CASE("Plant Library Model Building - maize") {
137 Context context;
138 PlantArchitecture plantarchitecture(&context);
139 plantarchitecture.disableMessages();
140 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("maize"));
141 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
142}
143
144DOCTEST_TEST_CASE("Plant Library Model Building - olive") {
145 Context context;
146 PlantArchitecture plantarchitecture(&context);
147 plantarchitecture.disableMessages();
148 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("olive"));
149 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
150}
151
152DOCTEST_TEST_CASE("Plant Library Model Building - pistachio") {
153 Context context;
154 PlantArchitecture plantarchitecture(&context);
155 plantarchitecture.disableMessages();
156 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("pistachio"));
157 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
158}
159
160DOCTEST_TEST_CASE("Plant Library Model Building - puncturevine") {
161 Context context;
162 PlantArchitecture plantarchitecture(&context);
163 plantarchitecture.disableMessages();
164 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("puncturevine"));
165 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
166}
167
168DOCTEST_TEST_CASE("Plant Library Model Building - easternredbud") {
169 Context context;
170 PlantArchitecture plantarchitecture(&context);
171 plantarchitecture.disableMessages();
172 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("easternredbud"));
173 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
174}
175
176DOCTEST_TEST_CASE("Plant Library Model Building - rice") {
177 Context context;
178 PlantArchitecture plantarchitecture(&context);
179 plantarchitecture.disableMessages();
180 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("rice"));
181 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
182}
183
184DOCTEST_TEST_CASE("Plant Library Model Building - butterlettuce") {
185 Context context;
186 PlantArchitecture plantarchitecture(&context);
187 plantarchitecture.disableMessages();
188 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("butterlettuce"));
189 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
190}
191
192DOCTEST_TEST_CASE("Plant Library Model Building - sorghum") {
193 Context context;
194 PlantArchitecture plantarchitecture(&context);
195 plantarchitecture.disableMessages();
196 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("sorghum"));
197 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
198}
199
200DOCTEST_TEST_CASE("Plant Library Model Building - soybean") {
201 Context context;
202 PlantArchitecture plantarchitecture(&context);
203 plantarchitecture.disableMessages();
204 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("soybean"));
205 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
206}
207
208DOCTEST_TEST_CASE("Plant Library Model Building - strawberry") {
209 Context context;
210 PlantArchitecture plantarchitecture(&context);
211 plantarchitecture.disableMessages();
212 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("strawberry"));
213 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
214}
215
216DOCTEST_TEST_CASE("Plant Library Model Building - sugarbeet") {
217 Context context;
218 PlantArchitecture plantarchitecture(&context);
219 plantarchitecture.disableMessages();
220 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("sugarbeet"));
221 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
222}
223
224DOCTEST_TEST_CASE("Plant Library Model Building - tomato") {
225 Context context;
226 PlantArchitecture plantarchitecture(&context);
227 plantarchitecture.disableMessages();
228 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("tomato"));
229 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
230}
231
232DOCTEST_TEST_CASE("Plant Library Model Building - walnut") {
233 Context context;
234 PlantArchitecture plantarchitecture(&context);
235 plantarchitecture.disableMessages();
236 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("walnut"));
237 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
238}
239
240DOCTEST_TEST_CASE("Plant Library Model Building - wheat") {
241 Context context;
242 PlantArchitecture plantarchitecture(&context);
243 plantarchitecture.disableMessages();
244 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("wheat"));
245 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
246}
247
248DOCTEST_TEST_CASE("PlantArchitecture writeTreeQSM") {
249 Context context;
250 PlantArchitecture plantarchitecture(&context);
251 plantarchitecture.disableMessages();
252
253 // Build a simple plant
254 plantarchitecture.loadPlantModelFromLibrary("bean");
255 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 50);
256
257 // Test writing TreeQSM format
258 std::string filename = "test_plant_qsm.txt";
259 DOCTEST_CHECK_NOTHROW(plantarchitecture.writeQSMCylinderFile(plantID, filename));
260
261 // Check that file was created and has correct format
262 std::ifstream file(filename);
263 DOCTEST_CHECK(file.good());
264
265 if (file.good()) {
266 std::string header_line;
267 std::getline(file, header_line);
268
269 // Check header contains expected columns
270 DOCTEST_CHECK(header_line.find("radius (m)") != std::string::npos);
271 DOCTEST_CHECK(header_line.find("length (m)") != std::string::npos);
272 DOCTEST_CHECK(header_line.find("start_point") != std::string::npos);
273 DOCTEST_CHECK(header_line.find("axis_direction") != std::string::npos);
274 DOCTEST_CHECK(header_line.find("branch") != std::string::npos);
275 DOCTEST_CHECK(header_line.find("branch_order") != std::string::npos);
276
277 // Check that there is at least one data line
278 std::string data_line;
279 bool has_data = static_cast<bool>(std::getline(file, data_line));
280 DOCTEST_CHECK(has_data);
281
282 if (has_data) {
283 // Count tab-separated values in data line
284 size_t tab_count = std::count(data_line.begin(), data_line.end(), '\t');
285 DOCTEST_CHECK(tab_count >= 12); // Should have at least 13 columns (12 tabs)
286 }
287
288 file.close();
289
290 // Clean up test file
291 std::remove(filename.c_str());
292 }
293}
294
295DOCTEST_TEST_CASE("PlantArchitecture writeTreeQSM invalid plant") {
296 capture_cerr cerr_buffer;
297 Context context;
298 PlantArchitecture plantarchitecture(&context);
299 plantarchitecture.disableMessages();
300
301 // Test with invalid plant ID
302 DOCTEST_CHECK_THROWS(plantarchitecture.writeQSMCylinderFile(999, "invalid_plant.txt"));
303}
304
305DOCTEST_TEST_CASE("PlantArchitecture pruneSolidBoundaryCollisions") {
306 Context context;
307 PlantArchitecture plantarchitecture(&context);
308 plantarchitecture.disableMessages();
309
310 // Enable collision detection first
311 plantarchitecture.enableSoftCollisionAvoidance();
312
313 // Load a plant model from library
314 plantarchitecture.loadPlantModelFromLibrary("tomato");
315
316 // Create a plant and let it grow first WITHOUT boundaries
317 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
318 plantarchitecture.advanceTime(plantID, 15); // Substantial growth to ensure objects exist
319
320 // Get object count after growth but before boundaries
321 std::vector<uint> objects_before_boundaries = plantarchitecture.getAllObjectIDs();
322 uint count_before_boundaries = objects_before_boundaries.size();
323
324 // Ensure we have some objects to work with
325 DOCTEST_CHECK(count_before_boundaries > 0);
326
327 // Now create solid boundaries that will definitely intersect with plant parts
328 // Place boundaries at z=0.05 to intersect with low-lying plant parts
329 std::vector<uint> boundary_UUIDs;
330 for (int i = -2; i <= 2; i++) {
331 for (int j = -2; j <= 2; j++) {
332 // Create a grid of triangles to ensure we catch plant parts
333 boundary_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.1f, j * 0.1f, 0.05f), make_vec3((i + 1) * 0.1f, j * 0.1f, 0.05f), make_vec3(i * 0.1f, (j + 1) * 0.1f, 0.05f)));
334 }
335 }
336
337 // Enable solid obstacle avoidance with the boundaries
338 plantarchitecture.enableSolidObstacleAvoidance(boundary_UUIDs, 0.2f);
339
340 // Trigger another growth step which should call pruneSolidBoundaryCollisions()
341 // Use a very small time step to minimize new growth
342 plantarchitecture.advanceTime(plantID, 0.1f); // Very small step to trigger pruning
343
344 // Get final object count
345 std::vector<uint> final_objects = plantarchitecture.getAllObjectIDs();
346 uint final_count = final_objects.size();
347
348 // Verify that objects were actually pruned by checking that we have fewer objects
349 // than we would expect if no pruning occurred. Since some growth may still happen,
350 // we check if the final count is reasonable given pruning occurred.
351 // The key test is that our implementation ran without errors and produced output
352 // indicating pruning occurred (visible in test output: "Pruned X objects").
353 DOCTEST_CHECK(final_count > 0); // Basic sanity check - we should still have some objects
354}
355
356DOCTEST_TEST_CASE("PlantArchitecture pruneSolidBoundaryCollisions no boundaries") {
357 Context context;
358 PlantArchitecture plantarchitecture(&context);
359 plantarchitecture.disableMessages();
360
361 // Load a plant model from library
362 plantarchitecture.loadPlantModelFromLibrary("tomato");
363
364 // Create a simple plant
365 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
366 plantarchitecture.advanceTime(plantID, 5);
367
368 // Get initial object count
369 std::vector<uint> initial_objects = plantarchitecture.getAllObjectIDs();
370 uint initial_count = initial_objects.size();
371
372 // Advance time again without boundaries - should not prune anything
373 plantarchitecture.advanceTime(plantID, 2);
374
375 // Check that no objects were pruned (may have grown more)
376 std::vector<uint> final_objects = plantarchitecture.getAllObjectIDs();
377 uint final_count = final_objects.size();
378
379 DOCTEST_CHECK(final_count >= initial_count);
380}
381
382DOCTEST_TEST_CASE("PlantArchitecture hard collision avoidance base stem protection") {
383 Context context;
384 PlantArchitecture plantarchitecture(&context);
385 plantarchitecture.disableMessages();
386
387 // Enable collision detection first
388 plantarchitecture.enableSoftCollisionAvoidance();
389
390 // Load a plant model from library
391 plantarchitecture.loadPlantModelFromLibrary("tomato");
392
393 // Create a plant that starts slightly below ground surface (e.g., at z = -0.05)
394 // This simulates the common scenario where ground model is slightly uneven
395 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, -0.05f), 0);
396
397 // Create ground surface as solid obstacle slightly above plant base
398 std::vector<uint> ground_UUIDs;
399
400 // Create a ground patch that the plant would intersect if it doesn't grow upward
401 for (int i = -2; i <= 2; i++) {
402 for (int j = -2; j <= 2; j++) {
403 ground_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.2f, j * 0.2f, 0.0f), // Ground at z=0
404 make_vec3((i + 1) * 0.2f, j * 0.2f, 0.0f), make_vec3(i * 0.2f, (j + 1) * 0.2f, 0.0f)));
405 ground_UUIDs.push_back(context.addTriangle(make_vec3((i + 1) * 0.2f, (j + 1) * 0.2f, 0.0f), make_vec3((i + 1) * 0.2f, j * 0.2f, 0.0f), make_vec3(i * 0.2f, (j + 1) * 0.2f, 0.0f)));
406 }
407 }
408
409 // Enable hard solid obstacle avoidance with the ground
410 plantarchitecture.enableSolidObstacleAvoidance(ground_UUIDs, 0.3f);
411
412 // Let the plant grow - it should grow upward despite starting below ground
413 // The first 3 nodes of the base stem should ignore solid obstacles
414 plantarchitecture.advanceTime(plantID, 10); // Sufficient growth time
415
416 // Get all plant objects to analyze growth direction
417 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
418 DOCTEST_CHECK(plant_objects.size() > 0);
419
420 // Calculate center of mass of all plant objects to verify upward growth
421 // If the plant made a U-turn downward, the center would be below the starting position
422 vec3 center_of_mass = make_vec3(0, 0, 0);
423 uint total_objects = 0;
424
425 for (uint objID: plant_objects) {
426 if (context.doesObjectExist(objID)) {
427 // Get object center using bounding box
428 vec3 min_corner, max_corner;
429 context.getObjectBoundingBox(objID, min_corner, max_corner);
430
431 vec3 object_center = (min_corner + max_corner) / 2.0f;
432
433 center_of_mass = center_of_mass + object_center;
434 total_objects++;
435 }
436 }
437
438 if (total_objects > 0) {
439 center_of_mass = center_of_mass / float(total_objects);
440
441 // The center of mass should be above the starting position (z = -0.05)
442 // This verifies the plant grew upward rather than making a U-turn downward
443 DOCTEST_CHECK(center_of_mass.z > -0.075f);
444
445 // The key test is that the plant didn't curve significantly downward (U-turn behavior)
446 // A U-turn would result in center of mass well below starting position (e.g., < -0.06)
447 // Any value above -0.045 indicates successful avoidance of U-turn behavior
448 DOCTEST_CHECK(center_of_mass.z > -0.075f); // Should not have made a U-turn downward
449 }
450
451 // Additional check: the plant should still exist (wasn't completely pruned)
452 // and should have a reasonable number of objects
453 DOCTEST_CHECK(plant_objects.size() >= 5); // Should have internodes, leaves, etc.
454}
455
456DOCTEST_TEST_CASE("PlantArchitecture enableSolidObstacleAvoidance fruit adjustment control") {
457 Context context;
458 PlantArchitecture plantarchitecture(&context);
459 plantarchitecture.disableMessages();
460
461 // Create some obstacles
462 std::vector<uint> obstacle_UUIDs;
463 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(-1, 1, 0)));
464 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(1, 1, 0), make_vec3(1, -1, 0), make_vec3(-1, 1, 0)));
465
466 // Test enabling solid obstacle avoidance with fruit adjustment enabled (default)
467 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.5f));
468
469 // Test enabling solid obstacle avoidance with fruit adjustment explicitly enabled
470 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.5f, true));
471
472 // Test enabling solid obstacle avoidance with fruit adjustment disabled
473 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.5f, false));
474
475 // Test with different avoidance distance and disabled fruit adjustment
476 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.3f, false));
477}
478
479DOCTEST_TEST_CASE("PlantArchitecture base stem protection with short internodes") {
480 Context context;
481 PlantArchitecture plantarchitecture(&context);
482 plantarchitecture.disableMessages();
483
484 // Enable collision detection first
485 plantarchitecture.enableSoftCollisionAvoidance();
486
487 // Load a plant model
488 plantarchitecture.loadPlantModelFromLibrary("tomato");
489
490 // Create a plant that starts at ground level
491 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
492
493 // Let it grow a small amount first to create some short internodes
494 plantarchitecture.advanceTime(plantID, 2);
495
496 // Create ground surface as solid obstacle
497 std::vector<uint> ground_UUIDs;
498 for (int i = -1; i <= 1; i++) {
499 for (int j = -1; j <= 1; j++) {
500 ground_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.3f, j * 0.3f, -0.01f), // Ground slightly below
501 make_vec3((i + 1) * 0.3f, j * 0.3f, -0.01f), make_vec3(i * 0.3f, (j + 1) * 0.3f, -0.01f)));
502 ground_UUIDs.push_back(context.addTriangle(make_vec3((i + 1) * 0.3f, (j + 1) * 0.3f, -0.01f), make_vec3((i + 1) * 0.3f, j * 0.3f, -0.01f), make_vec3(i * 0.3f, (j + 1) * 0.3f, -0.01f)));
503 }
504 }
505
506 // Enable solid obstacle avoidance with the ground
507 plantarchitecture.enableSolidObstacleAvoidance(ground_UUIDs, 0.2f);
508
509 // Let the plant grow more - it should grow normally despite having short internodes
510 // The length-based protection should kick in even if node count > 3
511 plantarchitecture.advanceTime(plantID, 8);
512
513 // Get all plant objects to verify plant survived and grew upward
514 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
515 DOCTEST_CHECK(plant_objects.size() > 0);
516
517 // Calculate center of mass to verify upward growth
518 vec3 center_of_mass = make_vec3(0, 0, 0);
519 uint total_objects = 0;
520
521 for (uint objID: plant_objects) {
522 if (context.doesObjectExist(objID)) {
523 vec3 min_corner, max_corner;
524 context.getObjectBoundingBox(objID, min_corner, max_corner);
525 vec3 object_center = (min_corner + max_corner) / 2.0f;
526 center_of_mass = center_of_mass + object_center;
527 total_objects++;
528 }
529 }
530
531 if (total_objects > 0) {
532 center_of_mass = center_of_mass / float(total_objects);
533
534 // The plant should have grown upward (center above ground level)
535 DOCTEST_CHECK(center_of_mass.z > 0.01f);
536
537 // Plant should have grown to a reasonable height, indicating protection worked
538 // Since we're testing short internodes, the height will be more modest
539 DOCTEST_CHECK(center_of_mass.z > 0.05f);
540 }
541
542 // Plant should have grown successfully (not been completely pruned)
543 DOCTEST_CHECK(plant_objects.size() >= 10);
544}
545
546DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Basic Functionality") {
547 Context context;
548 PlantArchitecture plantarchitecture(&context);
549 plantarchitecture.disableMessages();
550
551 // Enable collision detection for this test (optional - attraction points work independently)
552 plantarchitecture.enableSoftCollisionAvoidance();
553
554 // Test basic attraction points functionality
555 std::vector<vec3> attraction_points = {make_vec3(1.0f, 0.0f, 1.0f), make_vec3(0.0f, 1.0f, 1.5f)};
556
557 // Enable attraction points with valid parameters
558 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(attraction_points, 60.0f, 0.15f, 0.7f));
559
560 // Test parameter validation - invalid angle
561 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(0.0f, 0.1f, 0.5f));
562 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(190.0f, 0.1f, 0.5f));
563
564 // Test parameter validation - invalid distance
565 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, 0.0f, 0.5f));
566 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, -0.1f, 0.5f));
567
568 // Test parameter validation - invalid weight
569 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, 0.1f, -0.1f));
570 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, 0.1f, 1.1f));
571
572 // Update attraction points
573 std::vector<vec3> new_attraction_points = {make_vec3(2.0f, 0.0f, 2.0f)};
574 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints(new_attraction_points));
575
576 // Disable attraction points
577 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints());
578
579 // Test error when trying to update disabled attraction points
580 DOCTEST_CHECK_THROWS(plantarchitecture.updateAttractionPoints(new_attraction_points));
581}
582
583DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Independent of Collision Detection") {
584 Context context;
585 PlantArchitecture plantarchitecture(&context);
586 plantarchitecture.disableMessages();
587
588 std::vector<vec3> attraction_points = {make_vec3(1.0f, 0.0f, 1.0f)};
589
590 // Attraction points should work without collision detection enabled
591 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(attraction_points));
592}
593
594DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Empty Vector") {
595 Context context;
596 PlantArchitecture plantarchitecture(&context);
597 plantarchitecture.disableMessages();
598
599 std::vector<vec3> empty_attraction_points;
600
601 // Try to enable attraction points with empty vector
602 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(empty_attraction_points));
603
604 // Enable with valid points first (should work without collision detection)
605 std::vector<vec3> valid_points = {make_vec3(1.0f, 0.0f, 1.0f)};
606 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(valid_points));
607
608 // Try to update with empty vector (should fail)
609 DOCTEST_CHECK_THROWS(plantarchitecture.updateAttractionPoints(empty_attraction_points));
610}
611
612DOCTEST_TEST_CASE("PlantArchitecture Native Attraction Point Cone Detection") {
613 Context context;
614 PlantArchitecture plantarchitecture(&context);
615 plantarchitecture.disableMessages();
616
617 // Set up attraction points at known locations
618 std::vector<vec3> attraction_points = {
619 make_vec3(0.0f, 0.0f, 2.0f), // Directly ahead
620 make_vec3(1.0f, 0.0f, 1.0f), // Right and forward
621 make_vec3(-1.0f, 0.0f, 1.0f), // Left and forward
622 make_vec3(0.0f, 2.0f, 0.0f), // Far to the side (should be outside cone)
623 };
624
625 // Enable attraction points (should work without collision detection)
626 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(attraction_points, 60.0f, 3.0f, 0.7f));
627
628 // Test 1: Looking straight up should find the point directly ahead
629 vec3 vertex = make_vec3(0.0f, 0.0f, 0.0f);
630 vec3 look_direction = make_vec3(0.0f, 0.0f, 1.0f); // Looking up
631 vec3 direction_to_closest;
632
633 bool found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 60.0f, direction_to_closest);
634 DOCTEST_CHECK(found);
635
636 // The closest should be the one directly ahead (0,0,2)
637 vec3 expected_direction = make_vec3(0.0f, 0.0f, 1.0f);
638 float dot_product = direction_to_closest * expected_direction;
639 DOCTEST_CHECK(dot_product > 0.99f); // Should be very close to parallel
640
641 // Test 2: Looking to the side should NOT find the point far to the side (outside cone)
642 look_direction = make_vec3(1.0f, 0.0f, 0.0f); // Looking right
643 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 30.0f, direction_to_closest);
644
645 // With a narrow cone (30 degrees), the side point at (0,2,0) should be outside the cone
646 // But the point at (1,0,1) might be visible, so we might still find something
647
648 // Test 3: Test parameter validation
649 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, -1.0f, 60.0f, direction_to_closest);
650 DOCTEST_CHECK(!found); // Should fail with negative look ahead distance
651
652 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 0.0f, direction_to_closest);
653 DOCTEST_CHECK(!found); // Should fail with zero half angle
654
655 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 180.0f, direction_to_closest);
656 DOCTEST_CHECK(!found); // Should fail with 180 degree half angle
657}
658
659DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Plant Growth Integration") {
660 Context context;
661 PlantArchitecture plantarchitecture(&context);
662 plantarchitecture.disableMessages();
663
664 // Enable collision detection first
665 plantarchitecture.enableSoftCollisionAvoidance();
666
667 // Set up attraction points above the plant to guide upward growth
668 std::vector<vec3> attraction_points = {
669 make_vec3(0.1f, 0.1f, 1.0f), // Close to plant base but higher
670 make_vec3(0.0f, 0.0f, 1.5f) // Further away and higher
671 };
672
673 // Enable attraction points with moderate attraction weight
674 plantarchitecture.enableAttractionPoints(attraction_points, 80.0f, 0.2f, 0.6f);
675
676 // Create a simple plant
677 plantarchitecture.loadPlantModelFromLibrary("bean");
678 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
679
680 // Let the plant grow with attraction points enabled
681 plantarchitecture.advanceTime(plantID, 5);
682
683 // Get plant geometry to verify growth occurred
684 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
685 DOCTEST_CHECK(plant_objects.size() > 0);
686
687 // Calculate plant center of mass to verify upward growth toward attraction points
688 vec3 center_of_mass = make_vec3(0, 0, 0);
689 uint total_objects = 0;
690
691 for (uint objID: plant_objects) {
692 if (context.doesObjectExist(objID)) {
693 vec3 min_corner, max_corner;
694 context.getObjectBoundingBox(objID, min_corner, max_corner);
695 vec3 object_center = (min_corner + max_corner) / 2.0f;
696 center_of_mass = center_of_mass + object_center;
697 total_objects++;
698 }
699 }
700
701 if (total_objects > 0) {
702 center_of_mass = center_of_mass / float(total_objects);
703
704 // Plant should have grown upward toward attraction points
705 // Bean plants start small, so adjust expectations to realistic growth
706 DOCTEST_CHECK(center_of_mass.z > 0.01f); // At least 1cm above ground
707
708 // Plant should show some lateral movement toward attraction points
709 // (not perfectly vertical growth due to attraction)
710 float lateral_distance = sqrt(center_of_mass.x * center_of_mass.x + center_of_mass.y * center_of_mass.y);
711 DOCTEST_CHECK(lateral_distance >= 0.0f); // Basic sanity check
712 }
713
714 // Test disabling attraction points mid-growth
715 plantarchitecture.disableAttractionPoints();
716
717 // Continue growing - should revert to natural growth patterns
718 plantarchitecture.advanceTime(plantID, 3);
719
720 // Verify plant continues to exist and grow
721 std::vector<uint> final_plant_objects = plantarchitecture.getAllObjectIDs();
722 DOCTEST_CHECK(final_plant_objects.size() >= plant_objects.size());
723}
724
725DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Priority Over Collision Avoidance") {
726 Context context;
727 PlantArchitecture plantarchitecture(&context);
728 plantarchitecture.disableMessages();
729
730 // Create some obstacle geometry
731 std::vector<uint> obstacle_UUIDs;
732 for (int i = 0; i < 3; i++) {
733 for (int j = 0; j < 3; j++) {
734 obstacle_UUIDs.push_back(
735 context.addTriangle(make_vec3(i * 0.3f + 0.5f, j * 0.3f + 0.5f, 0.5f + i * 0.1f), make_vec3((i + 1) * 0.3f + 0.5f, (j + 1) * 0.3f + 0.5f, 0.5f + i * 0.1f), make_vec3((i + 1) * 0.3f + 0.5f, j * 0.3f + 0.5f, 0.5f + i * 0.1f)));
736 }
737 }
738
739 // Enable collision detection with obstacles
740 plantarchitecture.enableSoftCollisionAvoidance(obstacle_UUIDs);
741
742 // Set up attraction points on the opposite side of obstacles
743 std::vector<vec3> attraction_points = {
744 make_vec3(-0.5f, 0.0f, 1.0f) // Away from obstacles
745 };
746
747 // Enable attraction points - should override soft collision avoidance
748 plantarchitecture.enableAttractionPoints(attraction_points, 90.0f, 0.3f, 0.8f);
749
750 // Create a plant near obstacles
751 plantarchitecture.loadPlantModelFromLibrary("bean");
752 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0.3f, 0.3f, 0), 0);
753
754 // Let the plant grow - should be attracted away from obstacles
755 plantarchitecture.advanceTime(plantID, 4);
756
757 // Verify plant grew successfully (attraction points should guide it away from obstacles)
758 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
759 DOCTEST_CHECK(plant_objects.size() > 0);
760
761 // Check that plant moved toward attraction point (negative x direction)
762 vec3 center_of_mass = make_vec3(0, 0, 0);
763 uint total_objects = 0;
764
765 for (uint objID: plant_objects) {
766 if (context.doesObjectExist(objID)) {
767 vec3 min_corner, max_corner;
768 context.getObjectBoundingBox(objID, min_corner, max_corner);
769 vec3 object_center = (min_corner + max_corner) / 2.0f;
770 center_of_mass = center_of_mass + object_center;
771 total_objects++;
772 }
773 }
774
775 if (total_objects > 0) {
776 center_of_mass = center_of_mass / float(total_objects);
777
778 // Plant should have grown upward
779 DOCTEST_CHECK(center_of_mass.z > 0.01f); // At least 1cm above ground
780
781 // With strong attraction weight (0.8), plant should show movement toward attraction point
782 // This validates that attraction points override soft collision avoidance
783 }
784}
785
786DOCTEST_TEST_CASE("PlantArchitecture Hard Obstacle Avoidance Takes Priority Over Attraction Points") {
787 Context context;
788 PlantArchitecture plantarchitecture(&context);
789 plantarchitecture.disableMessages();
790
791 // Create ground-level obstacles that would trigger hard obstacle avoidance
792 std::vector<uint> solid_obstacle_UUIDs;
793 for (int i = -1; i <= 1; i++) {
794 for (int j = -1; j <= 1; j++) {
795 solid_obstacle_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.1f, j * 0.1f, 0.1f), make_vec3((i + 1) * 0.1f, (j + 1) * 0.1f, 0.1f), make_vec3((i + 1) * 0.1f, j * 0.1f, 0.1f)));
796 }
797 }
798
799 // Enable collision detection first
800 plantarchitecture.enableSoftCollisionAvoidance();
801
802 // Enable solid obstacle avoidance (hard obstacles)
803 plantarchitecture.enableSolidObstacleAvoidance(solid_obstacle_UUIDs, 0.15f);
804
805 // Set up attraction points in the opposite direction of safe growth
806 std::vector<vec3> attraction_points = {
807 make_vec3(0.0f, 0.0f, 0.05f) // Low attraction point that would conflict with obstacle avoidance
808 };
809
810 // Enable attraction points
811 plantarchitecture.enableAttractionPoints(attraction_points, 70.0f, 0.1f, 0.9f);
812
813 // Create a plant at the origin
814 plantarchitecture.loadPlantModelFromLibrary("bean");
815 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
816
817 // Let the plant grow - hard obstacle avoidance should take priority
818 plantarchitecture.advanceTime(plantID, 3);
819
820 // Verify plant grew successfully despite conflicting guidance
821 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
822 DOCTEST_CHECK(plant_objects.size() > 0);
823
824 // Plant should have grown upward to avoid hard obstacles, regardless of attraction points
825 vec3 center_of_mass = make_vec3(0, 0, 0);
826 uint total_objects = 0;
827
828 for (uint objID: plant_objects) {
829 if (context.doesObjectExist(objID)) {
830 vec3 min_corner, max_corner;
831 context.getObjectBoundingBox(objID, min_corner, max_corner);
832 vec3 object_center = (min_corner + max_corner) / 2.0f;
833 center_of_mass = center_of_mass + object_center;
834 total_objects++;
835 }
836 }
837
838 if (total_objects > 0) {
839 center_of_mass = center_of_mass / float(total_objects);
840
841 // Hard obstacle avoidance should force upward growth
842 DOCTEST_CHECK(center_of_mass.z > 0.01f); // At least 1cm above ground
843
844 // Plant should have avoided the low obstacles (which are at 0.1m height)
845 // So plant should be higher than the obstacle level
846 DOCTEST_CHECK(center_of_mass.z > 0.005f); // Above the base obstacle level
847 }
848}
849
850DOCTEST_TEST_CASE("PlantArchitecture Attraction Points with Surface Following") {
851 Context context;
852 PlantArchitecture plantarchitecture(&context);
853 plantarchitecture.disableMessages();
854
855 // Create a vertical wall that we want the plant to approach and then grow parallel to
856 std::vector<uint> wall_obstacle_UUIDs;
857 std::vector<vec3> wall_attraction_points;
858
859 // Create vertical wall at x = 0.3
860 for (int i = 0; i < 5; i++) {
861 for (int j = 0; j < 3; j++) {
862 // Wall surface obstacles (solid)
863 wall_obstacle_UUIDs.push_back(context.addTriangle(make_vec3(0.3f, i * 0.05f, j * 0.05f), make_vec3(0.3f, (i + 1) * 0.05f, (j + 1) * 0.05f), make_vec3(0.3f, (i + 1) * 0.05f, j * 0.05f)));
864
865 // Attraction points on the wall surface
866 wall_attraction_points.push_back(make_vec3(0.29f, i * 0.05f + 0.025f, j * 0.05f + 0.025f));
867 }
868 }
869
870 // Enable collision detection with wall obstacles
871 plantarchitecture.enableSoftCollisionAvoidance();
872
873 // Enable solid obstacle avoidance for the wall
874 plantarchitecture.enableSolidObstacleAvoidance(wall_obstacle_UUIDs, 0.05f);
875
876 // Enable attraction points on the wall surface with reduced obstacle reduction factor
877 // This allows the plant to maintain some attraction even when avoiding obstacles
878 plantarchitecture.enableAttractionPoints(wall_attraction_points, 60.0f, 0.1f, 0.8f);
879 plantarchitecture.setAttractionParameters(60.0f, 0.1f, 0.8f, 0.5f); // Higher obstacle reduction factor
880
881 // Create a plant at origin that should grow toward the wall
882 plantarchitecture.loadPlantModelFromLibrary("bean");
883 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
884
885 // Let the plant grow - it should approach the wall and then follow it
886 plantarchitecture.advanceTime(plantID, 4);
887
888 // Get plant geometry to verify behavior
889 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
890 DOCTEST_CHECK(plant_objects.size() > 0);
891
892 // Calculate plant center of mass
893 vec3 center_of_mass = make_vec3(0, 0, 0);
894 uint total_objects = 0;
895
896 for (uint objID: plant_objects) {
897 if (context.doesObjectExist(objID)) {
898 vec3 min_corner, max_corner;
899 context.getObjectBoundingBox(objID, min_corner, max_corner);
900 vec3 object_center = (min_corner + max_corner) / 2.0f;
901 center_of_mass = center_of_mass + object_center;
902 total_objects++;
903 }
904 }
905
906 if (total_objects > 0) {
907 center_of_mass = center_of_mass / float(total_objects);
908
909 // Plant should have grown upward
910 DOCTEST_CHECK(center_of_mass.z > 0.01f);
911
912 // The key test is that the plant grows successfully with both attraction points and obstacle avoidance enabled
913 // This validates that the new blended approach doesn't cause conflicts or crashes
914 // The exact movement direction depends on many factors, but the plant should grow
915
916 // This test primarily validates that our improved blending logic works without errors
917 // when both attraction points and hard obstacle avoidance are enabled simultaneously
918 }
919}
920
921DOCTEST_TEST_CASE("PlantArchitecture Smooth Hard Obstacle Avoidance") {
922 Context context;
923 PlantArchitecture plantarchitecture(&context);
924 plantarchitecture.disableMessages();
925
926 plantarchitecture.enableSoftCollisionAvoidance();
927 plantarchitecture.loadPlantModelFromLibrary("bean");
928
929 // Create obstacles at varying distances to test smooth avoidance behavior
930 std::vector<uint> obstacle_UUIDs;
931
932 // Create obstacles at different normalized distances from plant growth path
933 // Plant will grow upward from (0,0,0), so place obstacles to the side at different z heights
934 for (int i = 0; i < 4; i++) {
935 float z_height = 0.1f + i * 0.05f; // Heights: 0.1, 0.15, 0.2, 0.25
936
937 // Create obstacle patches at different distances from expected growth path
938 float x_distance = 0.05f + i * 0.02f; // Distances: 0.05, 0.07, 0.09, 0.11
939
940 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(x_distance, -0.02f, z_height), make_vec3(x_distance + 0.04f, -0.02f, z_height), make_vec3(x_distance, 0.02f, z_height)));
941 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(x_distance + 0.04f, 0.02f, z_height), make_vec3(x_distance + 0.04f, -0.02f, z_height), make_vec3(x_distance, 0.02f, z_height)));
942 }
943
944 plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.25f);
945
946 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
947 plantarchitecture.advanceTime(plantID, 8);
948
949 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
950 DOCTEST_CHECK(plant_objects.size() > 0);
951
952 // Calculate plant center of mass to verify it avoided obstacles
953 vec3 center_of_mass = make_vec3(0, 0, 0);
954 uint total_objects = 0;
955
956 for (uint objID: plant_objects) {
957 if (context.doesObjectExist(objID)) {
958 vec3 min_corner, max_corner;
959 context.getObjectBoundingBox(objID, min_corner, max_corner);
960 vec3 object_center = (min_corner + max_corner) / 2.0f;
961 center_of_mass = center_of_mass + object_center;
962 total_objects++;
963 }
964 }
965
966 if (total_objects > 0) {
967 center_of_mass = center_of_mass / float(total_objects);
968
969 // Plant should have grown upward successfully
970 DOCTEST_CHECK(center_of_mass.z > 0.01f);
971
972 // Plant should have moved away from obstacles (toward negative x since obstacles are on positive x side)
973 // This tests that smooth avoidance works without the harsh discrete jumps
974 DOCTEST_CHECK(center_of_mass.x <= 0.01f); // Should stay near or move away from obstacles
975
976 // Key validation: plant grows successfully with smooth obstacle avoidance
977 // The smooth distance-normalized approach should provide gradual, natural avoidance
978 // rather than abrupt discrete changes in behavior
979 }
980}
981
982DOCTEST_TEST_CASE("PlantArchitecture Hard Obstacle Avoidance Buffer Zone") {
983 Context context;
984 PlantArchitecture plantarchitecture(&context);
985 plantarchitecture.disableMessages();
986
987 plantarchitecture.enableSoftCollisionAvoidance();
988 plantarchitecture.loadPlantModelFromLibrary("bean");
989
990 // Create a vertical post obstacle similar to the test case image
991 std::vector<uint> post_UUIDs;
992 float post_radius = 0.02f; // 2cm radius post
993 float post_height = 0.5f; // 50cm tall post
994
995 // Create post as a series of triangles forming a cylinder at x=0.1m (10cm from plant center)
996 int segments = 8;
997 for (int i = 0; i < segments; i++) {
998 float theta1 = 2.0f * M_PI * float(i) / float(segments);
999 float theta2 = 2.0f * M_PI * float(i + 1) / float(segments);
1000
1001 vec3 p1_bottom = make_vec3(0.1f + post_radius * cos(theta1), post_radius * sin(theta1), 0);
1002 vec3 p2_bottom = make_vec3(0.1f + post_radius * cos(theta2), post_radius * sin(theta2), 0);
1003 vec3 p1_top = make_vec3(0.1f + post_radius * cos(theta1), post_radius * sin(theta1), post_height);
1004 vec3 p2_top = make_vec3(0.1f + post_radius * cos(theta2), post_radius * sin(theta2), post_height);
1005
1006 // Two triangles per segment to form cylinder walls
1007 post_UUIDs.push_back(context.addTriangle(p1_bottom, p2_bottom, p1_top));
1008 post_UUIDs.push_back(context.addTriangle(p2_bottom, p2_top, p1_top));
1009 }
1010
1011 // Set detection distance and enable solid obstacle avoidance
1012 float detection_distance = 0.2f; // 20cm detection distance
1013 float expected_buffer = detection_distance * 0.05f; // 5% buffer = 1cm
1014
1015 plantarchitecture.enableSolidObstacleAvoidance(post_UUIDs, detection_distance);
1016
1017 // Create plant at origin, should grow toward +x direction but avoid the post
1018 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1019 plantarchitecture.advanceTime(plantID, 8);
1020
1021 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
1022 DOCTEST_CHECK(plant_objects.size() > 0);
1023
1024 // Calculate minimum distance between plant and post to verify buffer is maintained
1025 float min_distance_to_post = std::numeric_limits<float>::max();
1026 vec3 post_center = make_vec3(0.1f, 0, 0.25f); // Center of post
1027
1028 for (uint objID: plant_objects) {
1029 if (context.doesObjectExist(objID)) {
1030 vec3 min_corner, max_corner;
1031 context.getObjectBoundingBox(objID, min_corner, max_corner);
1032
1033 // Check distance from each corner of plant object to post center
1034 vec3 corners[8] = {make_vec3(min_corner.x, min_corner.y, min_corner.z), make_vec3(max_corner.x, min_corner.y, min_corner.z), make_vec3(min_corner.x, max_corner.y, min_corner.z), make_vec3(min_corner.x, min_corner.y, max_corner.z),
1035 make_vec3(max_corner.x, max_corner.y, min_corner.z), make_vec3(max_corner.x, min_corner.y, max_corner.z), make_vec3(min_corner.x, max_corner.y, max_corner.z), make_vec3(max_corner.x, max_corner.y, max_corner.z)};
1036
1037 for (int i = 0; i < 8; i++) {
1038 float distance = (corners[i] - post_center).magnitude();
1039 min_distance_to_post = std::min(min_distance_to_post, distance);
1040 }
1041 }
1042 }
1043
1044 // Plant should maintain buffer distance from post (accounting for post radius)
1045 float expected_min_distance = post_radius + expected_buffer;
1046 DOCTEST_CHECK(min_distance_to_post >= expected_min_distance * 0.8f); // Allow 20% tolerance for growth dynamics
1047
1048 // Plant should have grown upward successfully despite obstacle
1049 vec3 plant_center = make_vec3(0, 0, 0);
1050 uint plant_object_count = 0;
1051
1052 for (uint objID: plant_objects) {
1053 if (context.doesObjectExist(objID)) {
1054 vec3 min_corner, max_corner;
1055 context.getObjectBoundingBox(objID, min_corner, max_corner);
1056 vec3 object_center = (min_corner + max_corner) / 2.0f;
1057 plant_center = plant_center + object_center;
1058 plant_object_count++;
1059 }
1060 }
1061
1062 if (plant_object_count > 0) {
1063 plant_center = plant_center / float(plant_object_count);
1064 DOCTEST_CHECK(plant_center.z > 0.01f); // Should grow upward
1065
1066 // Plant should avoid growing directly into the post (should stay away from x=0.1)
1067 // With buffer zone avoidance, plant should either go around or grow upward
1068 DOCTEST_CHECK(fabs(plant_center.x - 0.1f) > expected_buffer * 0.5f); // Should maintain some distance from post center line
1069 }
1070}
1071
1072DOCTEST_TEST_CASE("PlantArchitecture solid obstacle avoidance works independently") {
1073 Context context;
1074 PlantArchitecture plantarchitecture(&context);
1075 plantarchitecture.disableMessages();
1076
1077 // Create obstacle geometry (ground plane)
1078 std::vector<uint> obstacle_UUIDs;
1079 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(-1, -1, -0.01f), make_vec3(1, -1, -0.01f), make_vec3(-1, 1, -0.01f)));
1080 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(1, 1, -0.01f), make_vec3(1, -1, -0.01f), make_vec3(-1, 1, -0.01f)));
1081
1082 // Test: Enable ONLY solid obstacle avoidance (no soft collision avoidance)
1083 // This should work independently after our fix
1084 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.2f));
1085
1086 // Load and build a plant
1087 plantarchitecture.loadPlantModelFromLibrary("bean");
1088 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1089
1090 // Advance time - this should work without crashing and plant should grow upward
1091 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 5.0f));
1092
1093 // Verify plant was created and grew
1094 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
1095 DOCTEST_CHECK(plant_objects.size() > 0);
1096
1097 // Calculate plant center of mass to verify upward growth (avoiding ground obstacle)
1098 vec3 plant_center = make_vec3(0, 0, 0);
1099 uint plant_object_count = 0;
1100
1101 for (uint objID: plant_objects) {
1102 if (context.doesObjectExist(objID)) {
1103 vec3 min_corner, max_corner;
1104 context.getObjectBoundingBox(objID, min_corner, max_corner);
1105 vec3 object_center = (min_corner + max_corner) / 2.0f;
1106 plant_center = plant_center + object_center;
1107 plant_object_count++;
1108 }
1109 }
1110
1111 if (plant_object_count > 0) {
1112 plant_center = plant_center / float(plant_object_count);
1113 // Plant should grow upward, avoiding the ground obstacle at z = -0.01f
1114 DOCTEST_CHECK(plant_center.z > 0.01f);
1115 }
1116
1117 // Test: Add soft collision avoidance on top of existing solid obstacle avoidance
1118 // This should work together seamlessly
1119 std::vector<uint> soft_target_UUIDs;
1120 std::vector<uint> soft_target_IDs;
1121 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSoftCollisionAvoidance(soft_target_UUIDs, soft_target_IDs));
1122
1123 // Continue growing - should still work with both systems enabled
1124 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 2.0f));
1125
1126 // Verify plant continued to grow
1127 std::vector<uint> final_plant_objects = plantarchitecture.getAllObjectIDs();
1128 DOCTEST_CHECK(final_plant_objects.size() >= plant_objects.size());
1129}
1130
1131DOCTEST_TEST_CASE("PlantArchitecture Per-Plant Attraction Points") {
1132 Context context;
1133 PlantArchitecture plantarchitecture(&context);
1134
1135 // Disable messages for cleaner test output
1136 plantarchitecture.disableMessages();
1137
1138 // Create two plants at different positions
1139 uint plantID1 = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1140 uint plantID2 = plantarchitecture.addPlantInstance(make_vec3(5, 0, 0), 0);
1141
1142 // Set different attraction points for each plant
1143 std::vector<vec3> attraction_points_1 = {make_vec3(1.0f, 0.0f, 1.0f), make_vec3(0.0f, 1.0f, 1.5f)};
1144 std::vector<vec3> attraction_points_2 = {make_vec3(6.0f, 0.0f, 1.0f), make_vec3(5.0f, 1.0f, 1.5f)};
1145
1146 // Enable attraction points for each plant with different parameters
1147 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID1, attraction_points_1, 60.0f, 0.2f, 0.7f));
1148 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID2, attraction_points_2, 45.0f, 0.15f, 0.5f));
1149
1150 // Test parameter updates for individual plants
1151 DOCTEST_CHECK_NOTHROW(plantarchitecture.setAttractionParameters(plantID1, 80.0f, 0.25f, 0.8f, 0.6f));
1152 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints(plantID2, {make_vec3(6.5f, 0.5f, 2.0f)}));
1153 DOCTEST_CHECK_NOTHROW(plantarchitecture.appendAttractionPoints(plantID1, {make_vec3(1.5f, 1.5f, 2.0f)}));
1154
1155 // Test disabling for individual plants
1156 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints(plantID1));
1157
1158 // Test error handling for invalid plant IDs
1159 DOCTEST_CHECK_THROWS(plantarchitecture.enableAttractionPoints(9999, attraction_points_1));
1160 DOCTEST_CHECK_THROWS(plantarchitecture.disableAttractionPoints(9999));
1161 DOCTEST_CHECK_THROWS(plantarchitecture.updateAttractionPoints(9999, attraction_points_1));
1162 DOCTEST_CHECK_THROWS(plantarchitecture.appendAttractionPoints(9999, attraction_points_1));
1163 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(9999, 60.0f, 0.15f, 0.7f, 0.75f));
1164}
1165
1166DOCTEST_TEST_CASE("PlantArchitecture Global vs Per-Plant Interaction") {
1167 Context context;
1168 PlantArchitecture plantarchitecture(&context);
1169
1170 // Disable messages for cleaner test output
1171 plantarchitecture.disableMessages();
1172
1173 // Create a plant first
1174 uint plantID1 = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1175
1176 // Set global attraction points - should affect all plants including existing ones
1177 std::vector<vec3> global_attraction_points = {make_vec3(1.0f, 0.0f, 1.0f), make_vec3(0.0f, 1.0f, 1.5f)};
1178 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(global_attraction_points, 60.0f, 0.15f, 0.7f));
1179
1180 // Create another plant after global attraction points are set
1181 uint plantID2 = plantarchitecture.addPlantInstance(make_vec3(5, 0, 0), 0);
1182
1183 // Now set plant-specific attraction points for plant 1 - should override global for that plant
1184 std::vector<vec3> specific_attraction_points = {make_vec3(2.0f, 0.0f, 2.0f)};
1185 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID1, specific_attraction_points, 45.0f, 0.1f, 0.5f));
1186
1187 // Test that global update affects all plants with attraction points enabled
1188 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints({make_vec3(3.0f, 0.0f, 3.0f)}));
1189
1190 // Global disable should affect all plants
1191 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints());
1192
1193 // Re-enable global attraction points to test backward compatibility
1194 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(global_attraction_points));
1195}
1196
1197DOCTEST_TEST_CASE("PlantArchitecture Plant-Specific Attraction Points Validation") {
1198 Context context;
1199 PlantArchitecture plantarchitecture(&context);
1200
1201 // Disable messages for cleaner test output
1202 plantarchitecture.disableMessages();
1203
1204 // Create plants to test validation and method calls
1205 uint plantID1 = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1206 uint plantID2 = plantarchitecture.addPlantInstance(make_vec3(5, 0, 0), 0);
1207
1208 // Set different attraction points for each plant
1209 std::vector<vec3> attraction_points_1 = {make_vec3(1.0f, 0.0f, 1.0f)};
1210 std::vector<vec3> attraction_points_2 = {make_vec3(6.0f, 0.0f, 1.0f)};
1211
1212 // Test that plant-specific methods work correctly
1213 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID1, attraction_points_1));
1214 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID2, attraction_points_2));
1215
1216 // Test parameter validation
1217 DOCTEST_CHECK_THROWS(plantarchitecture.enableAttractionPoints(plantID1, {}, 60.0f, 0.15f, 0.7f)); // Empty vector
1218 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(plantID1, 0.0f, 0.15f, 0.7f)); // Invalid angle
1219 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(plantID1, 60.0f, 0.0f, 0.7f)); // Invalid distance
1220
1221 // Test successful parameter updates
1222 DOCTEST_CHECK_NOTHROW(plantarchitecture.setAttractionParameters(plantID1, 80.0f, 0.25f, 0.8f, 0.6f));
1223 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints(plantID2, {make_vec3(6.5f, 0.5f, 2.0f)}));
1224 DOCTEST_CHECK_NOTHROW(plantarchitecture.appendAttractionPoints(plantID1, {make_vec3(1.5f, 1.5f, 2.0f)}));
1225
1226 // Test disabling
1227 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints(plantID1));
1228}
1229
1230DOCTEST_TEST_CASE("PlantArchitecture removeShootFloralBuds") {
1231 Context context;
1232 PlantArchitecture plantarchitecture(&context);
1233 plantarchitecture.disableMessages();
1234
1235 // Test invalid plant ID - should throw
1236 capture_cerr cerr_buffer;
1237 DOCTEST_CHECK_THROWS(plantarchitecture.removeShootFloralBuds(9999, 0));
1238
1239 // Create a plant instance to test valid plant ID but invalid shoot ID
1240 uint plantID = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1241 DOCTEST_CHECK(plantID != -1);
1242
1243 // Test invalid shoot ID - should throw
1244 DOCTEST_CHECK_THROWS(plantarchitecture.removeShootFloralBuds(plantID, 9999));
1245}
1246
1247DOCTEST_TEST_CASE("PlantArchitecture XML write with flowers and fruit") {
1248 Context context;
1249 PlantArchitecture plantarchitecture(&context);
1250 plantarchitecture.disableMessages();
1251
1252 // Load tomato model (has flowers and fruit)
1253 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("tomato"));
1254
1255 // Build simple plant
1256 vec3 base_position(1.0f, 2.0f, 0.5f);
1257 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(base_position, 180);
1258 DOCTEST_CHECK(plantID != uint(-1));
1259
1260 // Write plant structure to XML (should not crash even if no flowers)
1261 std::string xml_filename = "test_plant_xml_write.xml";
1262 DOCTEST_CHECK_NOTHROW(plantarchitecture.writePlantStructureXML(plantID, xml_filename));
1263
1264 // Clean up test file
1265 std::remove(xml_filename.c_str());
1266}
1267
1268DOCTEST_TEST_CASE("PlantArchitecture child shoot rotation with multiple petioles per internode") {
1269 Context context;
1270 PlantArchitecture plantarchitecture(&context);
1271 plantarchitecture.disableMessages();
1272
1273 // Regression test for bug where child shoots from different petioles had the same rotation
1274 // The fix changed line 4778 in PlantArchitecture.cpp to use petioles_per_internode
1275 // instead of axillary_vegetative_buds.size() for calculating rotation offset
1276
1277 // Use bean plant which has 2 petioles per internode in the unifoliate stage
1278 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1279 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1280 DOCTEST_CHECK(plantID != uint(-1));
1281
1282 // Advance time to allow growth and child shoot formation
1283 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 10.0f));
1284
1285 // Verify plant created geometry (basic sanity check that build succeeded)
1286 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1287 DOCTEST_CHECK(all_primitives.size() > 0);
1288
1289 // If this test passes, the fix is working (plant builds without errors)
1290 // The actual visual verification of proper 180-degree offset would require
1291 // more complex geometric analysis that is beyond the scope of a unit test
1292}
1293
1294DOCTEST_TEST_CASE("PlantArchitecture plant_name optional object data") {
1295 Context context;
1296 PlantArchitecture plantarchitecture(&context);
1297 plantarchitecture.disableMessages();
1298
1299 // Enable plant_name optional object data
1300 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("plant_name"));
1301
1302 // Load and build a bean plant
1303 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1304 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1305 DOCTEST_CHECK(plantID != uint(-1));
1306
1307 // Verify plant name is set correctly
1308 std::string plant_name = plantarchitecture.getPlantName(plantID);
1309 DOCTEST_CHECK(plant_name == "bean");
1310
1311 // Advance time to create more organs
1312 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 10.0f));
1313
1314 // Get all object IDs
1315 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1316 DOCTEST_CHECK(all_primitives.size() > 0);
1317
1318 // Verify plant_name object data is set on primitives
1319 bool found_plant_name_data = false;
1320 for (uint objID: all_primitives) {
1321 if (context.doesObjectDataExist(objID, "plant_name")) {
1322 std::string obj_plant_name;
1323 context.getObjectData(objID, "plant_name", obj_plant_name);
1324 DOCTEST_CHECK(obj_plant_name == "bean");
1325 found_plant_name_data = true;
1326 }
1327 }
1328 DOCTEST_CHECK(found_plant_name_data);
1329}
1330
1331DOCTEST_TEST_CASE("PlantArchitecture plant_type tree classification") {
1332 Context context;
1333 PlantArchitecture plantarchitecture(&context);
1334 plantarchitecture.disableMessages();
1335
1336 // Enable plant_type optional object data
1337 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("plant_type"));
1338
1339 // Test tree classification
1340 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("almond"));
1341 uint treeID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1342 DOCTEST_CHECK(treeID != uint(-1));
1343
1344 std::vector<uint> tree_primitives = plantarchitecture.getAllObjectIDs();
1345 DOCTEST_CHECK(tree_primitives.size() > 0);
1346 bool found_tree_type = false;
1347 for (uint objID: tree_primitives) {
1348 if (context.doesObjectDataExist(objID, "plant_type")) {
1349 std::string plant_type;
1350 context.getObjectData(objID, "plant_type", plant_type);
1351 DOCTEST_CHECK(plant_type == "tree");
1352 found_tree_type = true;
1353 }
1354 }
1355 DOCTEST_CHECK(found_tree_type);
1356}
1357
1358DOCTEST_TEST_CASE("PlantArchitecture plant_type weed classification") {
1359 Context context;
1360 PlantArchitecture plantarchitecture(&context);
1361 plantarchitecture.disableMessages();
1362
1363 // Enable plant_type optional object data
1364 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("plant_type"));
1365
1366 // Test weed classification
1367 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bindweed"));
1368 uint weedID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1369 DOCTEST_CHECK(weedID != uint(-1));
1370
1371 std::vector<uint> weed_primitives = plantarchitecture.getAllObjectIDs();
1372 DOCTEST_CHECK(weed_primitives.size() > 0);
1373 bool found_weed_type = false;
1374 for (uint objID: weed_primitives) {
1375 if (context.doesObjectDataExist(objID, "plant_type")) {
1376 std::string plant_type;
1377 context.getObjectData(objID, "plant_type", plant_type);
1378 DOCTEST_CHECK(plant_type == "weed");
1379 found_weed_type = true;
1380 }
1381 }
1382 DOCTEST_CHECK(found_weed_type);
1383}
1384
1385DOCTEST_TEST_CASE("PlantArchitecture plant_type herbaceous classification") {
1386 Context context;
1387 PlantArchitecture plantarchitecture(&context);
1388 plantarchitecture.disableMessages();
1389
1390 // Enable plant_type optional object data
1391 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("plant_type"));
1392
1393 // Test herbaceous classification (default)
1394 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1395 uint herbaceousID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1396 DOCTEST_CHECK(herbaceousID != uint(-1));
1397
1398 std::vector<uint> herbaceous_primitives = plantarchitecture.getAllObjectIDs();
1399 DOCTEST_CHECK(herbaceous_primitives.size() > 0);
1400 bool found_herbaceous_type = false;
1401 for (uint objID: herbaceous_primitives) {
1402 if (context.doesObjectDataExist(objID, "plant_type")) {
1403 std::string plant_type;
1404 context.getObjectData(objID, "plant_type", plant_type);
1405 DOCTEST_CHECK(plant_type == "herbaceous");
1406 found_herbaceous_type = true;
1407 }
1408 }
1409 DOCTEST_CHECK(found_herbaceous_type);
1410}
1411
1412DOCTEST_TEST_CASE("PlantArchitecture plant_height optional object data") {
1413 Context context;
1414 PlantArchitecture plantarchitecture(&context);
1415 plantarchitecture.disableMessages();
1416
1417 // Enable plant_height optional object data
1418 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("plant_height"));
1419
1420 // Build a bean plant
1421 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1422 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1423 DOCTEST_CHECK(plantID != uint(-1));
1424
1425 // Get initial height
1426 float initial_height = plantarchitecture.getPlantHeight(plantID);
1427 DOCTEST_CHECK(initial_height > 0);
1428
1429 // Advance time to allow growth
1430 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 10.0f));
1431
1432 // Verify height increased
1433 float final_height = plantarchitecture.getPlantHeight(plantID);
1434 DOCTEST_CHECK(final_height > initial_height);
1435
1436 // Verify plant_height object data was set and is reasonable
1437 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1438 DOCTEST_CHECK(all_primitives.size() > 0);
1439 bool found_height_data = false;
1440 for (uint objID: all_primitives) {
1441 if (context.doesObjectDataExist(objID, "plant_height")) {
1442 float obj_height;
1443 context.getObjectData(objID, "plant_height", obj_height);
1444 // Check height is within reasonable range (close to final_height)
1445 DOCTEST_CHECK(obj_height > initial_height);
1446 DOCTEST_CHECK(std::abs(obj_height - final_height) < 0.01f);
1447 found_height_data = true;
1448 break; // Only need to check one primitive
1449 }
1450 }
1451 DOCTEST_CHECK(found_height_data);
1452}
1453
1454DOCTEST_TEST_CASE("PlantArchitecture phenology_stage optional object data") {
1455 Context context;
1456 PlantArchitecture plantarchitecture(&context);
1457 plantarchitecture.disableMessages();
1458
1459 // Enable phenology_stage optional object data
1460 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("phenology_stage"));
1461
1462 // Build a bean plant
1463 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1464 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1465 DOCTEST_CHECK(plantID != uint(-1));
1466
1467 // Initially should be vegetative (no flowers, not dormant)
1468 std::string initial_stage = plantarchitecture.determinePhenologyStage(plantID);
1469 DOCTEST_CHECK(initial_stage == "vegetative");
1470
1471 // Advance time to allow growth and potential flowering
1472 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 20.0f));
1473
1474 // Get current phenology stage
1475 std::string current_stage = plantarchitecture.determinePhenologyStage(plantID);
1476 DOCTEST_CHECK((current_stage == "vegetative" || current_stage == "reproductive" || current_stage == "senescent" || current_stage == "dormant"));
1477
1478 // Verify phenology_stage object data was set
1479 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1480 DOCTEST_CHECK(all_primitives.size() > 0);
1481 bool found_stage_data = false;
1482 for (uint objID: all_primitives) {
1483 if (context.doesObjectDataExist(objID, "phenology_stage")) {
1484 std::string obj_stage;
1485 context.getObjectData(objID, "phenology_stage", obj_stage);
1486 DOCTEST_CHECK(obj_stage == current_stage);
1487 found_stage_data = true;
1488 }
1489 }
1490 DOCTEST_CHECK(found_stage_data);
1491}
1492
1493DOCTEST_TEST_CASE("Build Parameters - Backward Compatibility (Grapevine VSP)") {
1494 // Test that empty parameter map produces identical plants to original hard-coded values
1495 Context context;
1496 PlantArchitecture plantarchitecture(&context);
1497 plantarchitecture.disableMessages();
1498
1499 // Build with default parameters (empty map)
1500 plantarchitecture.loadPlantModelFromLibrary("grapevine_VSP");
1501 std::map<std::string, float> empty_params;
1502 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0, empty_params);
1503
1504 // Verify plant was created
1505 DOCTEST_CHECK(plantID != uint(-1));
1506
1507 // Verify basic plant structure exists
1508 std::vector<uint> plant_primitives = plantarchitecture.getAllPlantObjectIDs(plantID);
1509 DOCTEST_CHECK(plant_primitives.size() > 0);
1510}
1511
1512DOCTEST_TEST_CASE("Build Parameters - Parameter Override (Grapevine VSP)") {
1513 // Test that custom parameter values are applied correctly
1514 Context context;
1515 PlantArchitecture plantarchitecture(&context);
1516 plantarchitecture.disableMessages();
1517
1518 // Build with custom parameters
1519 // Note: vine_spacing limited by cane max_nodes (9) * internode_length (0.15m) * 2 = 2.7m max
1520 plantarchitecture.loadPlantModelFromLibrary("grapevine_VSP");
1521 std::map<std::string, float> custom_params = {
1522 {"vine_spacing", 2.5f}, // 2.5m spacing (within max_nodes limit)
1523 {"trunk_height", 0.15f} // 15 cm trunk height
1524 };
1525 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0, custom_params);
1526
1527 // Verify plant was created with custom parameters
1528 DOCTEST_CHECK(plantID != uint(-1));
1529 std::vector<uint> plant_primitives = plantarchitecture.getAllPlantObjectIDs(plantID);
1530 DOCTEST_CHECK(plant_primitives.size() > 0);
1531}
1532
1533DOCTEST_TEST_CASE("Build Parameters - Validation Catches Invalid Values (Grapevine VSP)") {
1534 // Test that out-of-range values raise errors
1535 capture_cerr cerr_buffer;
1536 Context context;
1537 PlantArchitecture plantarchitecture(&context);
1538 plantarchitecture.disableMessages();
1539
1540 plantarchitecture.loadPlantModelFromLibrary("grapevine_VSP");
1541
1542 // Test vine_spacing out of range (valid range: 0.5-5.0)
1543 std::map<std::string, float> invalid_params1 = {{"vine_spacing", 10.0f}};
1544 DOCTEST_CHECK_THROWS(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0, invalid_params1));
1545
1546 // Test trunk_height out of range (valid range: 0.05-1.0)
1547 std::map<std::string, float> invalid_params2 = {{"trunk_height", 2.0f}};
1548 DOCTEST_CHECK_THROWS(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0, invalid_params2));
1549}
1550
1551DOCTEST_TEST_CASE("Build Parameters - Grapevine Wye Trellis Parameters") {
1552 // Test Wye grapevine specific trellis parameters
1553 Context context;
1554 PlantArchitecture plantarchitecture(&context);
1555 plantarchitecture.disableMessages();
1556
1557 plantarchitecture.loadPlantModelFromLibrary("grapevine_Wye");
1558 std::map<std::string, float> trellis_params = {
1559 {"trunk_height", 0.2f}, // 20 cm trunk height
1560 {"cordon_spacing", 0.8f}, // 80 cm between cordon rows
1561 {"vine_spacing", 2.0f}, // 2 m between plants
1562 {"catch_wire_height", 2.5f} // 2.5 m catch wire height
1563 };
1564 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0, trellis_params);
1565
1566 DOCTEST_CHECK(plantID != uint(-1));
1567 std::vector<uint> plant_primitives = plantarchitecture.getAllPlantObjectIDs(plantID);
1568 DOCTEST_CHECK(plant_primitives.size() > 0);
1569}
1570
1571DOCTEST_TEST_CASE("Build Parameters - Tree Training System (Almond)") {
1572 // Test tree training parameters
1573 Context context;
1574 PlantArchitecture plantarchitecture(&context);
1575 plantarchitecture.disableMessages();
1576
1577 // Note: trunk_height limited by trunk max_nodes (20) * internode_length (0.03m) = 0.6m max
1578 plantarchitecture.loadPlantModelFromLibrary("almond");
1579 std::map<std::string, float> tree_params = {
1580 {"trunk_height", 0.5f}, // 50 cm total trunk height (within max_nodes limit)
1581 {"num_scaffolds", 5.0f}, // 5 scaffold branches
1582 {"scaffold_angle", 35.0f} // 35 degree scaffold angle
1583 };
1584 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000, tree_params);
1585
1586 DOCTEST_CHECK(plantID != uint(-1));
1587 std::vector<uint> plant_primitives = plantarchitecture.getAllPlantObjectIDs(plantID);
1588 DOCTEST_CHECK(plant_primitives.size() > 0);
1589}
1590
1591DOCTEST_TEST_CASE("Build Parameters - Apple Tree") {
1592 // Test apple tree with custom parameters
1593 Context context;
1594 PlantArchitecture plantarchitecture(&context);
1595 plantarchitecture.disableMessages();
1596
1597 // Note: trunk_height limited by trunk max_nodes (20) * internode_length (0.04m) = 0.8m max
1598 plantarchitecture.loadPlantModelFromLibrary("apple");
1599 std::map<std::string, float> apple_params = {
1600 {"trunk_height", 0.7f}, // 70 cm trunk height (within max_nodes limit)
1601 {"num_scaffolds", 6.0f}, // 6 scaffold branches
1602 {"scaffold_angle", 45.0f} // 45 degree scaffold angle
1603 };
1604 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000, apple_params);
1605
1606 DOCTEST_CHECK(plantID != uint(-1));
1607}
1608
1609DOCTEST_TEST_CASE("Build Parameters - Pistachio Tree Fixed Scaffold System") {
1610 // Test pistachio tree with different scaffold count
1611 Context context;
1612 PlantArchitecture plantarchitecture(&context);
1613 plantarchitecture.disableMessages();
1614
1615 plantarchitecture.loadPlantModelFromLibrary("pistachio");
1616
1617 // Test with 2 scaffolds (minimum)
1618 std::map<std::string, float> pistachio_params_min = {{"num_scaffolds", 2.0f}};
1619 uint plantID_min = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000, pistachio_params_min);
1620 DOCTEST_CHECK(plantID_min != uint(-1));
1621
1622 // Test with 4 scaffolds (default)
1623 std::map<std::string, float> pistachio_params_def = {{"num_scaffolds", 4.0f}};
1624 uint plantID_def = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(5, 0, 0), 5000, pistachio_params_def);
1625 DOCTEST_CHECK(plantID_def != uint(-1));
1626}
1627
1628DOCTEST_TEST_CASE("Build Parameters - Canopy Building with Parameters") {
1629 // Test that parameters work with canopy building functions
1630 Context context;
1631 PlantArchitecture plantarchitecture(&context);
1632 plantarchitecture.disableMessages();
1633
1634 plantarchitecture.loadPlantModelFromLibrary("grapevine_VSP");
1635 std::map<std::string, float> canopy_params = {
1636 {"vine_spacing", 2.0f}, // 2.0m vine spacing
1637 {"trunk_height", 0.12f} // 12 cm trunk height
1638 };
1639
1640 // Test regular spacing canopy
1641 std::vector<uint> plantIDs = plantarchitecture.buildPlantCanopyFromLibrary(make_vec3(0, 0, 0), make_vec2(2, 2), make_int2(2, 2), 0, 1.0f, canopy_params);
1642
1643 DOCTEST_CHECK(plantIDs.size() == 4);
1644 for (uint plantID: plantIDs) {
1645 DOCTEST_CHECK(plantID != uint(-1));
1646 }
1647}
1648
1649DOCTEST_TEST_CASE("Build Parameters - Type Casting Float to Uint") {
1650 // Test that float parameters correctly cast to uint for node counts
1651 Context context;
1652 PlantArchitecture plantarchitecture(&context);
1653 plantarchitecture.disableMessages();
1654
1655 plantarchitecture.loadPlantModelFromLibrary("almond");
1656
1657 // Specify parameters as floats (should cast to uint internally where needed)
1658 // Note: trunk_height limited by trunk max_nodes (20) * internode_length (0.03m) = 0.6m max
1659 std::map<std::string, float> float_params = {
1660 {"trunk_height", 0.5f}, // Height as float (within max_nodes limit)
1661 {"num_scaffolds", 5.0f}, // Should cast to uint(5)
1662 {"scaffold_angle", 42.5f} // Angle as float
1663 };
1664
1665 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000, float_params);
1666 DOCTEST_CHECK(plantID != uint(-1));
1667}
1668
1669DOCTEST_TEST_CASE("PlantArchitecture optionalOutputObjectData 'all' keyword") {
1670 Context context;
1671 PlantArchitecture plantarchitecture(&context);
1672 plantarchitecture.disableMessages();
1673
1674 // Test that "all" (lowercase) enables all optional output data labels
1675 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("all"));
1676
1677 // Build a bean plant to verify data is actually being output
1678 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1679 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1680 DOCTEST_CHECK(plantID != uint(-1));
1681
1682 // Advance time to create some organs
1683 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 10.0f));
1684
1685 // Get all object IDs
1686 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1687 DOCTEST_CHECK(all_primitives.size() > 0);
1688
1689 // Verify that basic metadata labels are present (these exist on all plants)
1690 // Note: Organ-specific labels (peduncleID, flowerID, fruitID) may not exist
1691 // if the plant hasn't developed those organs yet at this age
1692 std::vector<std::string> expected_labels = {"age", "rank", "plantID", "plant_name", "plant_height", "plant_type", "phenology_stage", "leafID"};
1693
1694 for (const auto &label: expected_labels) {
1695 bool found = false;
1696 for (uint objID: all_primitives) {
1697 if (context.doesObjectDataExist(objID, label.c_str())) {
1698 found = true;
1699 break;
1700 }
1701 }
1702 DOCTEST_CHECK_MESSAGE(found, "Label '" << label << "' was not found on any primitive");
1703 }
1704}
1705
1706DOCTEST_TEST_CASE("PlantArchitecture optionalOutputObjectData 'all' case-insensitive") {
1707 // Test "ALL" (uppercase)
1708 {
1709 Context context;
1710 PlantArchitecture plantarchitecture(&context);
1711 plantarchitecture.disableMessages();
1712 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("ALL"));
1713 }
1714
1715 // Test "All" (mixed case)
1716 {
1717 Context context;
1718 PlantArchitecture plantarchitecture(&context);
1719 plantarchitecture.disableMessages();
1720 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("All"));
1721 }
1722
1723 // Test "aLl" (random mixed case)
1724 {
1725 Context context;
1726 PlantArchitecture plantarchitecture(&context);
1727 plantarchitecture.disableMessages();
1728 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("aLl"));
1729 }
1730}
1731
1732DOCTEST_TEST_CASE("PlantArchitecture optionalOutputObjectData invalid label throws error") {
1733 Context context;
1734 PlantArchitecture plantarchitecture(&context);
1735 plantarchitecture.disableMessages();
1736
1737 // Test that an invalid label throws a helios_runtime_error with descriptive message
1738 bool caught_error = false;
1739 try {
1740 plantarchitecture.optionalOutputObjectData("invalid_label");
1741 } catch (const std::exception &e) {
1742 caught_error = true;
1743 std::string error_msg(e.what());
1744 DOCTEST_CHECK(error_msg.find("invalid_label") != std::string::npos);
1745 DOCTEST_CHECK(error_msg.find("not a valid option") != std::string::npos);
1746 }
1747 DOCTEST_CHECK(caught_error);
1748
1749 // Note: helios_runtime_error() only writes to stderr when HELIOS_DEBUG is defined,
1750 // so we don't check stderr output here - just verify the exception is thrown correctly
1751}
1752
1753DOCTEST_TEST_CASE("PlantArchitecture optionalOutputObjectData vector with 'all'") {
1754 Context context;
1755 PlantArchitecture plantarchitecture(&context);
1756 plantarchitecture.disableMessages();
1757
1758 // Test that "all" works in a vector of labels
1759 std::vector<std::string> labels = {"all"};
1760 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData(labels));
1761
1762 // Build a bean plant to verify data is actually being output
1763 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1764 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1765 DOCTEST_CHECK(plantID != uint(-1));
1766
1767 // Advance time to create more organs
1768 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 10.0f));
1769
1770 // Get all object IDs
1771 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1772 DOCTEST_CHECK(all_primitives.size() > 0);
1773
1774 // Verify that at least a few optional output data labels are present
1775 bool found_age = false;
1776 bool found_rank = false;
1777 bool found_plant_name = false;
1778 for (uint objID: all_primitives) {
1779 if (context.doesObjectDataExist(objID, "age"))
1780 found_age = true;
1781 if (context.doesObjectDataExist(objID, "rank"))
1782 found_rank = true;
1783 if (context.doesObjectDataExist(objID, "plant_name"))
1784 found_plant_name = true;
1785 }
1786 DOCTEST_CHECK(found_age);
1787 DOCTEST_CHECK(found_rank);
1788 DOCTEST_CHECK(found_plant_name);
1789}
1790
1791DOCTEST_TEST_CASE("PlantArchitecture optionalOutputObjectData normal labels still work") {
1792 Context context;
1793 PlantArchitecture plantarchitecture(&context);
1794 plantarchitecture.disableMessages();
1795
1796 // Test that individual labels still work as expected
1797 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("age"));
1798 DOCTEST_CHECK_NOTHROW(plantarchitecture.optionalOutputObjectData("rank"));
1799
1800 // Build a bean plant
1801 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
1802 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1803 DOCTEST_CHECK(plantID != uint(-1));
1804
1805 // Advance time
1806 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 5.0f));
1807
1808 // Verify that age and rank data exist, but other optional data does not
1809 std::vector<uint> all_primitives = plantarchitecture.getAllObjectIDs();
1810 DOCTEST_CHECK(all_primitives.size() > 0);
1811
1812 bool found_age = false;
1813 bool found_rank = false;
1814 bool found_plant_name = false; // This should NOT be found
1815 for (uint objID: all_primitives) {
1816 if (context.doesObjectDataExist(objID, "age"))
1817 found_age = true;
1818 if (context.doesObjectDataExist(objID, "rank"))
1819 found_rank = true;
1820 if (context.doesObjectDataExist(objID, "plant_name"))
1821 found_plant_name = true;
1822 }
1823 DOCTEST_CHECK(found_age);
1824 DOCTEST_CHECK(found_rank);
1825 DOCTEST_CHECK_FALSE(found_plant_name); // Should NOT be enabled
1826}
1827
1828// ==================== NITROGEN MODEL TESTS ==================== //
1829
1830DOCTEST_TEST_CASE("Nitrogen Model - Initialization") {
1831 Context context;
1832 PlantArchitecture plantarchitecture(&context);
1833 plantarchitecture.disableMessages();
1834
1835 // Enable nitrogen model
1836 plantarchitecture.enableNitrogenModel();
1837 DOCTEST_CHECK(plantarchitecture.isNitrogenModelEnabled());
1838
1839 // Build a simple plant
1840 plantarchitecture.loadPlantModelFromLibrary("bean");
1841 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1842
1843 // Grow plant to create leaves
1844 plantarchitecture.advanceTime(plantID, 5.0f);
1845
1846 // Initialize nitrogen pools with target concentration
1847 float initial_N_concentration = 1.5f; // g N/m² (target value)
1848 plantarchitecture.initializePlantNitrogenPools(plantID, initial_N_concentration);
1849
1850 // Advance time to trigger nitrogen stress calculation and output writing
1851 plantarchitecture.advanceTime(plantID, 0.1f);
1852
1853 // Get all leaf objects
1854 std::vector<uint> all_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
1855 DOCTEST_CHECK(all_objects.size() > 0);
1856
1857 // Verify leaf nitrogen content was initialized
1858 bool found_leaf_N = false;
1859 for (uint objID: all_objects) {
1860 if (context.doesObjectDataExist(objID, "leaf_nitrogen_gN_m2")) {
1861 float leaf_N_area;
1862 context.getObjectData(objID, "leaf_nitrogen_gN_m2", leaf_N_area);
1863 DOCTEST_CHECK(leaf_N_area == doctest::Approx(initial_N_concentration).epsilon(0.1));
1864 found_leaf_N = true;
1865 }
1866 }
1867 DOCTEST_CHECK(found_leaf_N);
1868}
1869
1870DOCTEST_TEST_CASE("Nitrogen Model - Application and Pool Splitting") {
1871 Context context;
1872 PlantArchitecture plantarchitecture(&context);
1873 plantarchitecture.disableMessages();
1874
1875 plantarchitecture.enableNitrogenModel();
1876 plantarchitecture.loadPlantModelFromLibrary("bean");
1877 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1878 plantarchitecture.advanceTime(plantID, 3.0f);
1879
1880 // Initialize with zero nitrogen
1881 plantarchitecture.initializePlantNitrogenPools(plantID, 0.0f);
1882
1883 // Apply 10 g N to plant
1884 float N_applied = 10.0f; // g N
1885 plantarchitecture.addPlantNitrogen(plantID, N_applied);
1886
1887 // Verify nitrogen was split between root (15%) and available (85%) pools
1888 // We can't directly access the pools, but we can verify by advancing time
1889 // and checking that leaves accumulate nitrogen from the available pool
1890 plantarchitecture.advanceTime(plantID, 1.0f);
1891
1892 // Check that leaves now have nitrogen > 0
1893 std::vector<uint> all_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
1894 bool found_N_accumulation = false;
1895 for (uint objID: all_objects) {
1896 if (context.doesObjectDataExist(objID, "leaf_nitrogen_gN_m2")) {
1897 float leaf_N_area;
1898 context.getObjectData(objID, "leaf_nitrogen_gN_m2", leaf_N_area);
1899 if (leaf_N_area > 0) {
1900 found_N_accumulation = true;
1901 break;
1902 }
1903 }
1904 }
1905 DOCTEST_CHECK(found_N_accumulation);
1906}
1907
1908DOCTEST_TEST_CASE("Nitrogen Model - Rate Limiting") {
1909 Context context;
1910 PlantArchitecture plantarchitecture(&context);
1911 plantarchitecture.disableMessages();
1912
1913 plantarchitecture.enableNitrogenModel();
1914 plantarchitecture.loadPlantModelFromLibrary("bean");
1915 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1916 plantarchitecture.advanceTime(plantID, 5.0f);
1917
1918 // Initialize with zero nitrogen
1919 plantarchitecture.initializePlantNitrogenPools(plantID, 0.0f);
1920
1921 // Set nitrogen parameters with known max accumulation rate
1922 NitrogenParameters N_params;
1923 N_params.max_N_accumulation_rate = 0.1f; // g N/m²/day
1924 N_params.target_leaf_N_area = 10.0f; // Very high target to ensure demand > rate
1925 plantarchitecture.setPlantNitrogenParameters(plantID, N_params);
1926
1927 // Apply large amount of nitrogen
1928 plantarchitecture.addPlantNitrogen(plantID, 100.0f);
1929
1930 // Advance time by 1 day
1931 float dt = 1.0f;
1932 plantarchitecture.advanceTime(plantID, dt);
1933
1934 // Check that leaf nitrogen didn't exceed rate limit
1935 std::vector<uint> all_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
1936 for (uint objID: all_objects) {
1937 if (context.doesObjectDataExist(objID, "leaf_nitrogen_gN_m2")) {
1938 float leaf_N_area;
1939 context.getObjectData(objID, "leaf_nitrogen_gN_m2", leaf_N_area);
1940 // Should be at most max_N_accumulation_rate * dt
1941 DOCTEST_CHECK(leaf_N_area <= N_params.max_N_accumulation_rate * dt * 1.01f); // 1% tolerance
1942 }
1943 }
1944}
1945
1946DOCTEST_TEST_CASE("Nitrogen Model - Stress Factor Output") {
1947 Context context;
1948 PlantArchitecture plantarchitecture(&context);
1949 plantarchitecture.disableMessages();
1950
1951 plantarchitecture.enableNitrogenModel();
1952 plantarchitecture.loadPlantModelFromLibrary("bean");
1953 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1954 plantarchitecture.advanceTime(plantID, 5.0f);
1955
1956 // Initialize with low nitrogen (stress condition)
1957 plantarchitecture.initializePlantNitrogenPools(plantID, 0.5f); // Below target of 1.5
1958
1959 // Advance time to trigger stress factor calculation
1960 plantarchitecture.advanceTime(plantID, 0.1f);
1961
1962 // Verify stress factor exists and is in valid range [0, 1]
1963 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
1964 DOCTEST_CHECK(plant_objects.size() > 0);
1965
1966 bool found_stress_factor = false;
1967 for (uint objID: plant_objects) {
1968 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
1969 float stress_factor;
1970 context.getObjectData(objID, "nitrogen_stress_factor", stress_factor);
1971 DOCTEST_CHECK(stress_factor >= 0.0f);
1972 DOCTEST_CHECK(stress_factor <= 1.0f);
1973 // With low N, stress should be less than 1
1974 DOCTEST_CHECK(stress_factor < 1.0f);
1975 found_stress_factor = true;
1976 break;
1977 }
1978 }
1979 DOCTEST_CHECK(found_stress_factor);
1980}
1981
1982DOCTEST_TEST_CASE("Nitrogen Model - Remobilization") {
1983 Context context;
1984 PlantArchitecture plantarchitecture(&context);
1985 plantarchitecture.disableMessages();
1986
1987 plantarchitecture.enableNitrogenModel();
1988 plantarchitecture.loadPlantModelFromLibrary("bean");
1989 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1990
1991 // Grow plant to create leaves of different ages
1992 plantarchitecture.advanceTime(plantID, 15.0f);
1993
1994 // Initialize with low nitrogen to create stress condition
1995 plantarchitecture.initializePlantNitrogenPools(plantID, 0.8f); // Below target
1996
1997 // Advance time significantly to age leaves and trigger remobilization
1998 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 25.0f));
1999
2000 // Verify nitrogen stress factor reflects stress condition
2001 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
2002 bool found_stress_factor = false;
2003 for (uint objID: plant_objects) {
2004 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
2005 float stress_factor;
2006 context.getObjectData(objID, "nitrogen_stress_factor", stress_factor);
2007 DOCTEST_CHECK(stress_factor < 1.0f); // Should indicate some stress
2008 found_stress_factor = true;
2009 break;
2010 }
2011 }
2012 DOCTEST_CHECK(found_stress_factor);
2013}
2014
2015DOCTEST_TEST_CASE("Nitrogen Model - Fruit Removal") {
2016 Context context;
2017 PlantArchitecture plantarchitecture(&context);
2018 plantarchitecture.disableMessages();
2019
2020 plantarchitecture.enableNitrogenModel();
2021
2022 // Use tomato which produces fruit
2023 plantarchitecture.loadPlantModelFromLibrary("tomato");
2024 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2025
2026 // Grow plant to vegetative stage
2027 plantarchitecture.advanceTime(plantID, 30.0f);
2028
2029 // Initialize with adequate nitrogen
2030 plantarchitecture.initializePlantNitrogenPools(plantID, 1.5f);
2031
2032 // Add nitrogen to available pool
2033 plantarchitecture.addPlantNitrogen(plantID, 50.0f);
2034
2035 // Continue growth to allow fruiting
2036 plantarchitecture.advanceTime(plantID, 40.0f);
2037
2038 // Verify plant grew (basic sanity check)
2039 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
2040 DOCTEST_CHECK(plant_objects.size() > 0);
2041
2042 // Nitrogen stress factor should exist
2043 bool found_stress_factor = false;
2044 for (uint objID: plant_objects) {
2045 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
2046 found_stress_factor = true;
2047 break;
2048 }
2049 }
2050 DOCTEST_CHECK(found_stress_factor);
2051}
2052
2053DOCTEST_TEST_CASE("Nitrogen Model - Full Growth Cycle Integration") {
2054 Context context;
2055 PlantArchitecture plantarchitecture(&context);
2056 plantarchitecture.disableMessages();
2057
2058 plantarchitecture.enableNitrogenModel();
2059 plantarchitecture.loadPlantModelFromLibrary("bean");
2060 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2061
2062 // Initial growth
2063 plantarchitecture.advanceTime(plantID, 5.0f);
2064
2065 // Initialize nitrogen
2066 plantarchitecture.initializePlantNitrogenPools(plantID, 1.0f);
2067
2068 // Simulate periodic nitrogen applications during growth
2069 for (int i = 0; i < 5; i++) {
2070 plantarchitecture.addPlantNitrogen(plantID, 5.0f); // Add 5 g N
2071 plantarchitecture.advanceTime(plantID, 5.0f); // Grow 5 days
2072 }
2073
2074 // Verify plant completed growth cycle
2075 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
2076 DOCTEST_CHECK(plant_objects.size() > 0);
2077
2078 // Verify stress factor updated throughout
2079 bool found_stress_factor = false;
2080 float final_stress = 0;
2081 for (uint objID: plant_objects) {
2082 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
2083 context.getObjectData(objID, "nitrogen_stress_factor", final_stress);
2084 found_stress_factor = true;
2085 break;
2086 }
2087 }
2088 DOCTEST_CHECK(found_stress_factor);
2089 DOCTEST_CHECK(final_stress >= 0.0f);
2090 DOCTEST_CHECK(final_stress <= 1.0f);
2091
2092 // Verify leaves have nitrogen data
2093 bool found_leaf_N = false;
2094 for (uint objID: plant_objects) {
2095 if (context.doesObjectDataExist(objID, "leaf_nitrogen_gN_m2")) {
2096 float leaf_N;
2097 context.getObjectData(objID, "leaf_nitrogen_gN_m2", leaf_N);
2098 DOCTEST_CHECK(leaf_N >= 0.0f);
2099 found_leaf_N = true;
2100 }
2101 }
2102 DOCTEST_CHECK(found_leaf_N);
2103}
2104
2105DOCTEST_TEST_CASE("Nitrogen Model - Edge Case: Zero Nitrogen") {
2106 Context context;
2107 PlantArchitecture plantarchitecture(&context);
2108 plantarchitecture.disableMessages();
2109
2110 plantarchitecture.enableNitrogenModel();
2111 plantarchitecture.loadPlantModelFromLibrary("bean");
2112 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2113 plantarchitecture.advanceTime(plantID, 5.0f);
2114
2115 // Initialize with zero nitrogen - should not crash
2116 DOCTEST_CHECK_NOTHROW(plantarchitecture.initializePlantNitrogenPools(plantID, 0.0f));
2117
2118 // Advance time with zero nitrogen - should not crash
2119 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 5.0f));
2120
2121 // Stress factor should be very low (severe stress)
2122 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
2123 bool found_stress_factor = false;
2124 for (uint objID: plant_objects) {
2125 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
2126 float stress_factor;
2127 context.getObjectData(objID, "nitrogen_stress_factor", stress_factor);
2128 DOCTEST_CHECK(stress_factor < 0.2f); // Should be low under zero N
2129 found_stress_factor = true;
2130 break;
2131 }
2132 }
2133 DOCTEST_CHECK(found_stress_factor);
2134}
2135
2136DOCTEST_TEST_CASE("Nitrogen Model - Edge Case: Excessive Nitrogen") {
2137 Context context;
2138 PlantArchitecture plantarchitecture(&context);
2139 plantarchitecture.disableMessages();
2140
2141 plantarchitecture.enableNitrogenModel();
2142 plantarchitecture.loadPlantModelFromLibrary("bean");
2143 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2144 plantarchitecture.advanceTime(plantID, 5.0f);
2145
2146 // Initialize with zero
2147 plantarchitecture.initializePlantNitrogenPools(plantID, 0.0f);
2148
2149 // Set high accumulation rate to overcome rate limiting
2150 NitrogenParameters N_params;
2151 N_params.max_N_accumulation_rate = 1.0f; // g N/m²/day (10x default)
2152 plantarchitecture.setPlantNitrogenParameters(plantID, N_params);
2153
2154 // Apply excessive nitrogen - should not crash
2155 DOCTEST_CHECK_NOTHROW(plantarchitecture.addPlantNitrogen(plantID, 1000.0f));
2156
2157 // Advance time - should not crash
2158 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 5.0f));
2159
2160 // Stress factor should clamp at 1.0 (no stress) and be high with excess N
2161 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
2162 bool found_stress_factor = false;
2163 for (uint objID: plant_objects) {
2164 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
2165 float stress_factor;
2166 context.getObjectData(objID, "nitrogen_stress_factor", stress_factor);
2167 DOCTEST_CHECK(stress_factor <= 1.0f); // Should clamp at 1.0
2168 DOCTEST_CHECK(stress_factor >= 0.90f); // Should be very high with excess N and fast accumulation
2169 found_stress_factor = true;
2170 break;
2171 }
2172 }
2173 DOCTEST_CHECK(found_stress_factor);
2174}
2175
2176DOCTEST_TEST_CASE("Nitrogen Model - Edge Case: No Leaves") {
2177 Context context;
2178 PlantArchitecture plantarchitecture(&context);
2179 plantarchitecture.disableMessages();
2180
2181 plantarchitecture.enableNitrogenModel();
2182
2183 // Build plant at very early stage (no leaves yet)
2184 uint plantID = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
2185
2186 // Try to initialize nitrogen - should not crash even with no leaves
2187 DOCTEST_CHECK_NOTHROW(plantarchitecture.initializePlantNitrogenPools(plantID, 1.5f));
2188
2189 // Add nitrogen - should not crash
2190 DOCTEST_CHECK_NOTHROW(plantarchitecture.addPlantNitrogen(plantID, 10.0f));
2191
2192 // Advance time with no leaves - should not crash
2193 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 1.0f));
2194}
2195
2196DOCTEST_TEST_CASE("Nitrogen Model - Division by Zero Prevention") {
2197 Context context;
2198 PlantArchitecture plantarchitecture(&context);
2199 plantarchitecture.disableMessages();
2200
2201 plantarchitecture.enableNitrogenModel();
2202 plantarchitecture.loadPlantModelFromLibrary("bean");
2203 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2204
2205 // Grow plant slightly to create very small leaves
2206 plantarchitecture.advanceTime(plantID, 0.5f);
2207
2208 // Initialize nitrogen
2209 plantarchitecture.initializePlantNitrogenPools(plantID, 1.5f);
2210
2211 // Add nitrogen and advance - should handle small/zero leaf areas gracefully
2212 plantarchitecture.addPlantNitrogen(plantID, 10.0f);
2213
2214 // This should not crash due to division by zero (bug fix verification)
2215 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 1.0f));
2216
2217 // Continue growth and check remobilization doesn't crash either
2218 plantarchitecture.advanceTime(plantID, 20.0f);
2219 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 5.0f));
2220}
2221
2222DOCTEST_TEST_CASE("Nitrogen Model - Enable/Disable") {
2223 Context context;
2224 PlantArchitecture plantarchitecture(&context);
2225 plantarchitecture.disableMessages();
2226
2227 // Initially disabled
2228 DOCTEST_CHECK_FALSE(plantarchitecture.isNitrogenModelEnabled());
2229
2230 // Enable
2231 plantarchitecture.enableNitrogenModel();
2232 DOCTEST_CHECK(plantarchitecture.isNitrogenModelEnabled());
2233
2234 // Disable
2235 plantarchitecture.disableNitrogenModel();
2236 DOCTEST_CHECK_FALSE(plantarchitecture.isNitrogenModelEnabled());
2237
2238 // Build plant with model disabled - should not output nitrogen data
2239 plantarchitecture.loadPlantModelFromLibrary("bean");
2240 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2241 plantarchitecture.advanceTime(plantID, 5.0f);
2242
2243 std::vector<uint> plant_objects = plantarchitecture.getAllPlantObjectIDs(plantID);
2244 bool found_nitrogen_data = false;
2245 for (uint objID: plant_objects) {
2246 if (context.doesObjectDataExist(objID, "nitrogen_stress_factor")) {
2247 found_nitrogen_data = true;
2248 break;
2249 }
2250 }
2251 DOCTEST_CHECK_FALSE(found_nitrogen_data); // Should NOT have nitrogen data when disabled
2252}
2253
2254// ===== Tests for listShootTypeLabels() methods =====
2255
2256DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - no parameter success") {
2257 Context context;
2258 PlantArchitecture plantarchitecture(&context);
2259
2260 plantarchitecture.loadPlantModelFromLibrary("bean");
2261 std::vector<std::string> labels = plantarchitecture.listShootTypeLabels();
2262
2263 DOCTEST_CHECK(labels.size() == 2);
2264 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "unifoliate") != labels.end());
2265 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "trifoliate") != labels.end());
2266}
2267
2268DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - no parameter error") {
2269 std::string error_message;
2270 {
2271 capture_cerr cerr_buffer;
2272 Context context;
2273 PlantArchitecture plantarchitecture(&context);
2274
2275 // Should throw because no plant model is loaded
2276 DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.listShootTypeLabels()));
2277 }
2278}
2279
2280DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - string parameter success") {
2281 Context context;
2282 PlantArchitecture plantarchitecture(&context);
2283
2284 // Query bean shoot types without loading it
2285 std::vector<std::string> bean_labels = plantarchitecture.listShootTypeLabels("bean");
2286 DOCTEST_CHECK(bean_labels.size() == 2);
2287 DOCTEST_CHECK(std::find(bean_labels.begin(), bean_labels.end(), "unifoliate") != bean_labels.end());
2288 DOCTEST_CHECK(std::find(bean_labels.begin(), bean_labels.end(), "trifoliate") != bean_labels.end());
2289
2290 // Query tomato shoot types
2291 std::vector<std::string> tomato_labels = plantarchitecture.listShootTypeLabels("tomato");
2292 DOCTEST_CHECK(tomato_labels.size() == 1);
2293 DOCTEST_CHECK(std::find(tomato_labels.begin(), tomato_labels.end(), "mainstem") != tomato_labels.end());
2294}
2295
2296DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - string parameter error") {
2297 std::string error_message;
2298 {
2299 capture_cerr cerr_buffer;
2300 Context context;
2301 PlantArchitecture plantarchitecture(&context);
2302
2303 // Should throw for non-existent plant model
2304 DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.listShootTypeLabels("nonexistent_plant")));
2305 }
2306}
2307
2308DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - state preservation") {
2309 Context context;
2310 PlantArchitecture plantarchitecture(&context);
2311
2312 // Load bean plant model
2313 plantarchitecture.loadPlantModelFromLibrary("bean");
2314
2315 // Query tomato shoot types (should not change current plant model)
2316 std::vector<std::string> tomato_labels = plantarchitecture.listShootTypeLabels("tomato");
2317
2318 // Verify bean is still loaded by checking current labels
2319 std::vector<std::string> current_labels = plantarchitecture.listShootTypeLabels();
2320 DOCTEST_CHECK(current_labels.size() == 2);
2321 DOCTEST_CHECK(std::find(current_labels.begin(), current_labels.end(), "unifoliate") != current_labels.end());
2322 DOCTEST_CHECK(std::find(current_labels.begin(), current_labels.end(), "trifoliate") != current_labels.end());
2323}
2324
2325DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - all plant models") {
2326 Context context;
2327 PlantArchitecture plantarchitecture(&context);
2328
2329 std::vector<std::string> all_plants = plantarchitecture.getAvailablePlantModels();
2330
2331 // Should successfully query shoot types for all plants
2332 for (const auto &plant: all_plants) {
2333 std::vector<std::string> labels;
2334 DOCTEST_CHECK_NOTHROW(labels = plantarchitecture.listShootTypeLabels(plant));
2335 DOCTEST_CHECK(!labels.empty()); // All plants should have at least one shoot type
2336 }
2337}
2338
2339DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - uint parameter success") {
2340 Context context;
2341 PlantArchitecture plantarchitecture(&context);
2342
2343 // Load and build bean plant
2344 plantarchitecture.loadPlantModelFromLibrary("bean");
2345 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2346
2347 // Query by plantID
2348 std::vector<std::string> labels = plantarchitecture.listShootTypeLabels(plantID);
2349
2350 // Should match bean model shoot types
2351 DOCTEST_CHECK(labels.size() == 2);
2352 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "unifoliate") != labels.end());
2353 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "trifoliate") != labels.end());
2354}
2355
2356DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - uint parameter error") {
2357 std::string error_message;
2358 {
2359 capture_cerr cerr_buffer;
2360 Context context;
2361 PlantArchitecture plantarchitecture(&context);
2362
2363 // Should throw for invalid plantID
2364 DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.listShootTypeLabels(999)));
2365 }
2366}
2367
2368DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - multiple instances") {
2369 Context context;
2370 PlantArchitecture plantarchitecture(&context);
2371
2372 // Build bean plant
2373 plantarchitecture.loadPlantModelFromLibrary("bean");
2374 uint bean_plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
2375
2376 // Build tomato plant
2377 plantarchitecture.loadPlantModelFromLibrary("tomato");
2378 uint tomato_plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(1, 0, 0), 0);
2379
2380 // Verify each returns correct labels for its model
2381 std::vector<std::string> bean_labels = plantarchitecture.listShootTypeLabels(bean_plantID);
2382 DOCTEST_CHECK(bean_labels.size() == 2);
2383 DOCTEST_CHECK(std::find(bean_labels.begin(), bean_labels.end(), "unifoliate") != bean_labels.end());
2384 DOCTEST_CHECK(std::find(bean_labels.begin(), bean_labels.end(), "trifoliate") != bean_labels.end());
2385
2386 std::vector<std::string> tomato_labels = plantarchitecture.listShootTypeLabels(tomato_plantID);
2387 DOCTEST_CHECK(tomato_labels.size() == 1);
2388 DOCTEST_CHECK(std::find(tomato_labels.begin(), tomato_labels.end(), "mainstem") != tomato_labels.end());
2389}
2390
2391DOCTEST_TEST_CASE("PlantArchitecture getPlantInternodeObjectIDs with shoot type filter") {
2392 Context context;
2393 PlantArchitecture plantarchitecture(&context);
2394
2395 // Build a bean plant (has two shoot types: "unifoliate" and "trifoliate")
2396 plantarchitecture.loadPlantModelFromLibrary("bean");
2397 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0.0);
2398
2399 // Get all internode object IDs without filter
2400 std::vector<uint> all_internodes = plantarchitecture.getPlantInternodeObjectIDs(plantID);
2401 DOCTEST_CHECK(all_internodes.size() > 0);
2402
2403 // Get internode object IDs for "unifoliate" shoot type
2404 std::vector<uint> unifoliate_internodes = plantarchitecture.getPlantInternodeObjectIDs(plantID, "unifoliate");
2405 DOCTEST_CHECK(unifoliate_internodes.size() > 0);
2406
2407 // Get internode object IDs for "trifoliate" shoot type
2408 std::vector<uint> trifoliate_internodes = plantarchitecture.getPlantInternodeObjectIDs(plantID, "trifoliate");
2409 DOCTEST_CHECK(trifoliate_internodes.size() > 0);
2410
2411 // Verify that filtered results are subsets of all internodes
2412 for (uint objID : unifoliate_internodes) {
2413 DOCTEST_CHECK(std::find(all_internodes.begin(), all_internodes.end(), objID) != all_internodes.end());
2414 }
2415 for (uint objID : trifoliate_internodes) {
2416 DOCTEST_CHECK(std::find(all_internodes.begin(), all_internodes.end(), objID) != all_internodes.end());
2417 }
2418
2419 // Verify no overlap between unifoliate and trifoliate internodes
2420 for (uint objID : unifoliate_internodes) {
2421 DOCTEST_CHECK(std::find(trifoliate_internodes.begin(), trifoliate_internodes.end(), objID) == trifoliate_internodes.end());
2422 }
2423
2424 // Verify that sum of filtered internodes equals total internodes
2425 DOCTEST_CHECK(unifoliate_internodes.size() + trifoliate_internodes.size() == all_internodes.size());
2426}
2427
2428DOCTEST_TEST_CASE("PlantArchitecture getPlantInternodeObjectIDs with shoot type filter - error cases") {
2429 std::string error_message;
2430 {
2431 capture_cerr cerr_buffer;
2432 Context context;
2433 PlantArchitecture plantarchitecture(&context);
2434
2435 plantarchitecture.loadPlantModelFromLibrary("bean");
2436 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0.0);
2437
2438 // Should throw for non-existent shoot type
2439 DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.getPlantInternodeObjectIDs(plantID, "nonexistent_shoot_type")));
2440
2441 // Should throw for invalid plant ID
2442 DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.getPlantInternodeObjectIDs(9999, "unifoliate")));
2443 }
2444}
2445
2446int PlantArchitecture::selfTest(int argc, char **argv) {
2447 return helios::runDoctestWithValidation(argc, argv);
2448}