1.3.49
 
Loading...
Searching...
No Matches
InputOutput.cpp
Go to the documentation of this file.
1
16#include "PlantArchitecture.h"
17
18using namespace helios;
19
20std::string PlantArchitecture::makeShootString(const std::string &current_string, const std::shared_ptr<Shoot> &shoot, const std::vector<std::shared_ptr<Shoot>> &shoot_tree) const {
21
22 std::string outstring = current_string;
23
24 if (shoot->parent_shoot_ID != -1) {
25 outstring += "[";
26 }
27
28 outstring += "{" + std::to_string(rad2deg(shoot->base_rotation.pitch)) + "," + std::to_string(rad2deg(shoot->base_rotation.yaw)) + "," + std::to_string(rad2deg(shoot->base_rotation.roll)) + "," +
29 std::to_string(shoot->shoot_parameters.gravitropic_curvature.val() /*\todo */) + "," + shoot->shoot_type_label + "}";
30
31 uint node_number = 0;
32 for (auto &phytomer: shoot->phytomers) {
33
34 float length = phytomer->getInternodeLength();
35 float radius = phytomer->getInternodeRadius();
36
37 outstring += "Internode(" + std::to_string(length) + "," + std::to_string(radius) + "," + std::to_string(rad2deg(phytomer->internode_pitch)) + "," + std::to_string(rad2deg(phytomer->internode_phyllotactic_angle)) + ")";
38
39 for (uint petiole = 0; petiole < phytomer->petiole_length.size(); petiole++) {
40
41 outstring += "Petiole(" + std::to_string(phytomer->petiole_length.at(petiole)) + "," + std::to_string(phytomer->petiole_radii.at(petiole).front()) + "," + std::to_string(rad2deg(phytomer->petiole_pitch.at(petiole))) + ")";
42
43 //\todo If leaf is compound, just using rotation for the first leaf for now rather than adding multiple 'Leaf()' strings for each leaflet.
44 outstring += "Leaf(" + std::to_string(phytomer->leaf_size_max.at(petiole).front() * phytomer->current_leaf_scale_factor.at(petiole)) + "," + std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().pitch)) + "," +
45 std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().yaw)) + "," + std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().roll)) + ")";
46
47 if (shoot->childIDs.find(node_number) != shoot->childIDs.end()) {
48 for (int childID: shoot->childIDs.at(node_number)) {
49 outstring = makeShootString(outstring, shoot_tree.at(childID), shoot_tree);
50 }
51 }
52 }
53
54 node_number++;
55 }
56
57 if (shoot->parent_shoot_ID != -1) {
58 outstring += "]";
59 }
60
61 return outstring;
62}
63
64std::string PlantArchitecture::getPlantString(uint plantID) const {
65
66 auto plant_shoot_tree = &plant_instances.at(plantID).shoot_tree;
67
68 std::string out_string;
69
70 for (auto &shoot: *plant_shoot_tree) {
71 out_string = makeShootString(out_string, shoot, *plant_shoot_tree);
72 }
73
74 return out_string;
75}
76
77void PlantArchitecture::parseShootArgument(const std::string &shoot_argument, const std::map<std::string, PhytomerParameters> &phytomer_parameters, ShootParameters &shoot_parameters, AxisRotation &base_rotation, std::string &phytomer_label) {
78
79 // shoot argument order {}:
80 // 1. shoot base pitch/insertion angle (degrees)
81 // 2. shoot base yaw angle (degrees)
82 // 3. shoot roll angle (degrees)
83 // 4. gravitropic curvature (degrees/meter)
84 // 5. [optional] phytomer parameters string
85
86 size_t pos_shoot_start = 0;
87
88 std::string s_argument = shoot_argument;
89
90 pos_shoot_start = s_argument.find(',');
91 if (pos_shoot_start == std::string::npos) {
92 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
93 }
94 float insertion_angle = std::stof(s_argument.substr(0, pos_shoot_start));
95 s_argument.erase(0, pos_shoot_start + 1);
96 base_rotation.pitch = deg2rad(insertion_angle);
97
98 pos_shoot_start = s_argument.find(',');
99 if (pos_shoot_start == std::string::npos) {
100 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
101 }
102 float shoot_yaw = std::stof(s_argument.substr(0, pos_shoot_start));
103 s_argument.erase(0, pos_shoot_start + 1);
104 base_rotation.yaw = deg2rad(shoot_yaw);
105
106 pos_shoot_start = s_argument.find(',');
107 if (pos_shoot_start == std::string::npos) {
108 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
109 }
110 float shoot_roll = std::stof(s_argument.substr(0, pos_shoot_start));
111 s_argument.erase(0, pos_shoot_start + 1);
112 base_rotation.roll = deg2rad(shoot_roll);
113
114 pos_shoot_start = s_argument.find(',');
115 if (pos_shoot_start == std::string::npos) {
116 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
117 }
118 float shoot_curvature = std::stof(s_argument.substr(0, pos_shoot_start));
119 s_argument.erase(0, pos_shoot_start + 1);
120 shoot_parameters.gravitropic_curvature = shoot_curvature;
121
122 if (pos_shoot_start != std::string::npos) { // shoot type argument was given
123 pos_shoot_start = s_argument.find(',');
124 phytomer_label = s_argument.substr(0, pos_shoot_start);
125 s_argument.erase(0, pos_shoot_start + 1);
126 if (phytomer_parameters.find(phytomer_label) == phytomer_parameters.end()) {
127 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Phytomer parameters with label " + phytomer_label + " was not provided to PlantArchitecture::generatePlantFromString().");
128 }
129 shoot_parameters.phytomer_parameters = phytomer_parameters.at(phytomer_label);
130 } else { // shoot type argument not given - use first phytomer parameters in the map
131 phytomer_label = phytomer_parameters.begin()->first;
132 shoot_parameters.phytomer_parameters = phytomer_parameters.begin()->second;
133 }
134}
135
136void PlantArchitecture::parseInternodeArgument(const std::string &internode_argument, float &internode_radius, float &internode_length, PhytomerParameters &phytomer_parameters) {
137
138 // internode argument order Internode():
139 // 1. internode length (m)
140 // 2. internode radius (m)
141 // 3. internode pitch (degrees)
142 // 4. phyllotactic angle (degrees)
143
144 size_t pos_inode_start = 0;
145
146 std::string inode_argument = internode_argument;
147
148 pos_inode_start = inode_argument.find(',');
149 if (pos_inode_start == std::string::npos) {
150 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
151 }
152 internode_length = std::stof(inode_argument.substr(0, pos_inode_start));
153 inode_argument.erase(0, pos_inode_start + 1);
154
155 pos_inode_start = inode_argument.find(',');
156 if (pos_inode_start == std::string::npos) {
157 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
158 }
159 internode_radius = std::stof(inode_argument.substr(0, pos_inode_start));
160 inode_argument.erase(0, pos_inode_start + 1);
161
162 pos_inode_start = inode_argument.find(',');
163 if (pos_inode_start == std::string::npos) {
164 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
165 }
166 float internode_pitch = std::stof(inode_argument.substr(0, pos_inode_start));
167 inode_argument.erase(0, pos_inode_start + 1);
168 phytomer_parameters.internode.pitch = internode_pitch;
169
170 pos_inode_start = inode_argument.find(',');
171 if (pos_inode_start != std::string::npos) {
172 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
173 }
174 float internode_phyllotaxis = std::stof(inode_argument.substr(0, pos_inode_start));
175 inode_argument.erase(0, pos_inode_start + 1);
176 phytomer_parameters.internode.phyllotactic_angle = internode_phyllotaxis;
177}
178
179void PlantArchitecture::parsePetioleArgument(const std::string &petiole_argument, PhytomerParameters &phytomer_parameters) {
180
181 // petiole argument order Petiole():
182 // 1. petiole length (m)
183 // 2. petiole radius (m)
184 // 3. petiole pitch (degrees)
185
186 if (petiole_argument.empty()) {
187 phytomer_parameters.petiole.length = 0;
188 return;
189 }
190
191 size_t pos_petiole_start = 0;
192
193 std::string pet_argument = petiole_argument;
194
195 pos_petiole_start = pet_argument.find(',');
196 if (pos_petiole_start == std::string::npos) {
197 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
198 }
199 float petiole_length = std::stof(pet_argument.substr(0, pos_petiole_start));
200 pet_argument.erase(0, pos_petiole_start + 1);
201 phytomer_parameters.petiole.length = petiole_length;
202
203 pos_petiole_start = pet_argument.find(',');
204 if (pos_petiole_start == std::string::npos) {
205 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
206 }
207 float petiole_radius = std::stof(pet_argument.substr(0, pos_petiole_start));
208 pet_argument.erase(0, pos_petiole_start + 1);
209 phytomer_parameters.petiole.radius = petiole_radius;
210
211 pos_petiole_start = pet_argument.find(',');
212 if (pos_petiole_start != std::string::npos) {
213 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
214 }
215 float petiole_pitch = std::stof(pet_argument.substr(0, pos_petiole_start));
216 pet_argument.erase(0, pos_petiole_start + 1);
217 phytomer_parameters.petiole.pitch = petiole_pitch;
218}
219
220void PlantArchitecture::parseLeafArgument(const std::string &leaf_argument, PhytomerParameters &phytomer_parameters) {
221
222 // leaf argument order Leaf():
223 // 1. leaf scale factor
224 // 2. leaf pitch (degrees)
225 // 3. leaf yaw (degrees)
226 // 4. leaf roll (degrees)
227
228 if (leaf_argument.empty()) {
229 phytomer_parameters.leaf.prototype_scale = 0;
230 return;
231 }
232
233 size_t pos_leaf_start = 0;
234
235 std::string l_argument = leaf_argument;
236
237 pos_leaf_start = l_argument.find(',');
238 if (pos_leaf_start == std::string::npos) {
239 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
240 }
241 float leaf_scale = std::stof(l_argument.substr(0, pos_leaf_start));
242 l_argument.erase(0, pos_leaf_start + 1);
243 phytomer_parameters.leaf.prototype_scale = leaf_scale;
244
245 pos_leaf_start = l_argument.find(',');
246 if (pos_leaf_start == std::string::npos) {
247 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
248 }
249 float leaf_pitch = std::stof(l_argument.substr(0, pos_leaf_start));
250 l_argument.erase(0, pos_leaf_start + 1);
251 phytomer_parameters.leaf.pitch = leaf_pitch;
252
253 pos_leaf_start = l_argument.find(',');
254 if (pos_leaf_start == std::string::npos) {
255 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
256 }
257 float leaf_yaw = std::stof(l_argument.substr(0, pos_leaf_start));
258 l_argument.erase(0, pos_leaf_start + 1);
259 phytomer_parameters.leaf.yaw = leaf_yaw;
260
261 pos_leaf_start = l_argument.find(',');
262 if (pos_leaf_start != std::string::npos) {
263 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
264 }
265 float leaf_roll = std::stof(l_argument.substr(0, pos_leaf_start));
266 l_argument.erase(0, pos_leaf_start + 1);
267 phytomer_parameters.leaf.roll = leaf_roll;
268}
269
270size_t findShootClosingBracket(const std::string &lstring) {
271
272 size_t pos_close = std::string::npos;
273 size_t pos_open = lstring.find_first_of('[', 0);
274 if (pos_open == std::string::npos) {
275 return pos_close;
276 }
277
278 size_t pos = pos_open;
279 int count = 1;
280 while (count > 0) {
281 pos++;
282 if (lstring[pos] == '[') {
283 count++;
284 } else if (lstring[pos] == ']') {
285 count--;
286 }
287 if (pos == lstring.length()) {
288 return pos_close;
289 }
290 }
291 if (count == 0) {
292 pos_close = pos;
293 }
294 return pos_close; // Return the position of the closing bracket
295}
296
297void PlantArchitecture::parseStringShoot(const std::string &LString_shoot, uint plantID, int parentID, uint parent_node, const std::map<std::string, PhytomerParameters> &phytomer_parameters, ShootParameters &shoot_parameters) {
298
299 std::string lstring_tobeparsed = LString_shoot;
300
301 size_t pos_inode_start = 0;
302 std::string inode_delimiter = "Internode(";
303 std::string petiole_delimiter = "Petiole(";
304 std::string leaf_delimiter = "Leaf(";
305 bool base_shoot = true;
306 uint baseID;
307 AxisRotation shoot_base_rotation;
308
309 // parse shoot arguments
310 if (LString_shoot.front() != '{') {
311 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly. All shoots should start with a curly bracket containing two arguments {X,Y}.");
312 }
313 size_t pos_shoot_end = lstring_tobeparsed.find('}');
314 std::string shoot_argument = lstring_tobeparsed.substr(1, pos_shoot_end - 1);
315 std::string phytomer_label;
316 parseShootArgument(shoot_argument, phytomer_parameters, shoot_parameters, shoot_base_rotation, phytomer_label);
317 lstring_tobeparsed.erase(0, pos_shoot_end + 1);
318
319 uint shoot_node_count = 0;
320 while ((pos_inode_start = lstring_tobeparsed.find(inode_delimiter)) != std::string::npos) {
321
322 if (pos_inode_start != 0) {
323 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly.");
324 }
325
326 size_t pos_inode_end = lstring_tobeparsed.find(')');
327 std::string inode_argument = lstring_tobeparsed.substr(pos_inode_start + inode_delimiter.length(), pos_inode_end - pos_inode_start - inode_delimiter.length());
328 float internode_radius = 0;
329 float internode_length = 0;
330 parseInternodeArgument(inode_argument, internode_radius, internode_length, shoot_parameters.phytomer_parameters);
331 lstring_tobeparsed.erase(0, pos_inode_end + 1);
332
333 size_t pos_petiole_start = lstring_tobeparsed.find(petiole_delimiter);
334 size_t pos_petiole_end = lstring_tobeparsed.find(')');
335 std::string petiole_argument;
336 if (pos_petiole_start == 0) {
337 petiole_argument = lstring_tobeparsed.substr(pos_petiole_start + petiole_delimiter.length(), pos_petiole_end - pos_petiole_start - petiole_delimiter.length());
338 } else {
339 petiole_argument = "";
340 }
341 parsePetioleArgument(petiole_argument, shoot_parameters.phytomer_parameters);
342 if (pos_petiole_start == 0) {
343 lstring_tobeparsed.erase(0, pos_petiole_end + 1);
344 }
345
346 size_t pos_leaf_start = lstring_tobeparsed.find(leaf_delimiter);
347 size_t pos_leaf_end = lstring_tobeparsed.find(')');
348 std::string leaf_argument;
349 if (pos_leaf_start == 0) {
350 leaf_argument = lstring_tobeparsed.substr(pos_leaf_start + leaf_delimiter.length(), pos_leaf_end - pos_leaf_start - leaf_delimiter.length());
351 } else {
352 leaf_argument = "";
353 }
354 parseLeafArgument(leaf_argument, shoot_parameters.phytomer_parameters);
355 if (pos_leaf_start == 0) {
356 lstring_tobeparsed.erase(0, pos_leaf_end + 1);
357 }
358
359 // override phytomer creation function
360 shoot_parameters.phytomer_parameters.phytomer_creation_function = nullptr;
361
362 if (base_shoot) { // this is the first phytomer of the shoot
363 // defineShootType("shoot_"+phytomer_label, shoot_parameters);
364 defineShootType(phytomer_label, shoot_parameters); //*testing*
365
366 if (parentID < 0) { // this is the first shoot of the plant
367 // baseID = addBaseStemShoot(plantID, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, "shoot_" + phytomer_label);
368 baseID = addBaseStemShoot(plantID, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, phytomer_label); //*testing*
369 } else { // this is a child of an existing shoot
370 // baseID = addChildShoot(plantID, parentID, parent_node, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, "shoot_" + phytomer_label, 0);
371 baseID = addChildShoot(plantID, parentID, parent_node, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, phytomer_label, 0); //*testing*
372 }
373
374 base_shoot = false;
375 } else {
376 appendPhytomerToShoot(plantID, baseID, shoot_parameters.phytomer_parameters, internode_radius, internode_length, 1, 1);
377 }
378
379 while (!lstring_tobeparsed.empty() && lstring_tobeparsed.substr(0, 1) == "[") {
380 size_t pos_shoot_bracket_end = findShootClosingBracket(lstring_tobeparsed);
381 if (pos_shoot_bracket_end == std::string::npos) {
382 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly. Shoots must be closed with a ']'.");
383 }
384 std::string lstring_child = lstring_tobeparsed.substr(1, pos_shoot_bracket_end - 1);
385 parseStringShoot(lstring_child, plantID, (int) baseID, shoot_node_count, phytomer_parameters, shoot_parameters);
386 lstring_tobeparsed.erase(0, pos_shoot_bracket_end + 1);
387 }
388
389 shoot_node_count++;
390 }
391}
392
393uint PlantArchitecture::generatePlantFromString(const std::string &generation_string, const PhytomerParameters &phytomer_parameters) {
394 std::map<std::string, PhytomerParameters> phytomer_parameters_map;
395 phytomer_parameters_map["default"] = phytomer_parameters;
396 return generatePlantFromString(generation_string, phytomer_parameters_map);
397}
398
399uint PlantArchitecture::generatePlantFromString(const std::string &generation_string, const std::map<std::string, PhytomerParameters> &phytomer_parameters) {
400
401 // check that first characters are 'Internode'
402 if (generation_string.front() != '{') {
403 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): First character of string must be '{'");
404 }
405
406 ShootParameters shoot_parameters(context_ptr->getRandomGenerator());
407 shoot_parameters.max_nodes = 200;
408 shoot_parameters.vegetative_bud_break_probability_min = 0;
409 shoot_parameters.vegetative_bud_break_time = 0;
410 shoot_parameters.phyllochron_min = 0;
411
412 // assign default phytomer parameters. This can be changed later if the optional phytomer parameters label is provided in the shoot argument '{}'.
413 if (phytomer_parameters.empty()) {
414 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Phytomer parameters must be provided.");
415 }
416
417 uint plantID;
418
419 plantID = addPlantInstance(nullorigin, 0);
420
421 size_t pos_first_child_shoot = generation_string.find('[');
422
423 if (pos_first_child_shoot == std::string::npos) {
424 pos_first_child_shoot = generation_string.length();
425 }
426
427 parseStringShoot(generation_string, plantID, -1, 0, phytomer_parameters, shoot_parameters);
428
429
430 return plantID;
431}
432
433void PlantArchitecture::writePlantStructureXML(uint plantID, const std::string &filename) const {
434
435 if (plant_instances.find(plantID) == plant_instances.end()) {
436 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Plant ID " + std::to_string(plantID) + " does not exist.");
437 }
438
439 std::string output_file = filename;
440 if (!validateOutputPath(output_file, {".xml", ".XML"})) {
441 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
442 } else if (getFileName(output_file).empty()) {
443 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): The output file given was a directory. This argument should be the path to a file not to a directory.");
444 }
445
446 std::ofstream output_xml(filename);
447
448 if (!output_xml.is_open()) {
449 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
450 }
451
452 output_xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
453 output_xml << "<helios>" << std::endl;
454 output_xml << "\t<plant_instance ID=\"" << plantID << "\">" << std::endl;
455
456 output_xml << "\t\t<base_position> " << plant_instances.at(plantID).base_position.x << " " << plant_instances.at(plantID).base_position.y << " " << plant_instances.at(plantID).base_position.z << " </base_position>" << std::endl;
457 output_xml << "\t\t<plant_age> " << plant_instances.at(plantID).current_age << " </plant_age>" << std::endl;
458
459 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
460
461 output_xml << "\t\t<shoot ID=\"" << shoot->ID << "\">" << std::endl;
462 output_xml << "\t\t\t<shoot_type_label> " << shoot->shoot_type_label << " </shoot_type_label>" << std::endl;
463 output_xml << "\t\t\t<parent_shoot_ID> " << shoot->parent_shoot_ID << " </parent_shoot_ID>" << std::endl;
464 output_xml << "\t\t\t<parent_node_index> " << shoot->parent_node_index << " </parent_node_index>" << std::endl;
465 output_xml << "\t\t\t<parent_petiole_index> " << shoot->parent_petiole_index << " </parent_petiole_index>" << std::endl;
466 output_xml << "\t\t\t<base_rotation> " << rad2deg(shoot->base_rotation.pitch) << " " << rad2deg(shoot->base_rotation.yaw) << " " << rad2deg(shoot->base_rotation.roll) << " </base_rotation>" << std::endl;
467
468 for (auto &phytomer: shoot->phytomers) {
469
470 output_xml << "\t\t\t<phytomer>" << std::endl;
471 output_xml << "\t\t\t\t<internode>" << std::endl;
472 output_xml << "\t\t\t\t\t<internode_length>" << phytomer->getInternodeLength() << "</internode_length>" << std::endl;
473 output_xml << "\t\t\t\t\t<internode_radius>" << phytomer->getInternodeRadius() << "</internode_radius>" << std::endl;
474 output_xml << "\t\t\t\t\t<internode_pitch>" << rad2deg(phytomer->internode_pitch) << "</internode_pitch>" << std::endl;
475 output_xml << "\t\t\t\t\t<internode_phyllotactic_angle>" << rad2deg(phytomer->internode_phyllotactic_angle) << "</internode_phyllotactic_angle>" << std::endl;
476
477 for (uint petiole = 0; petiole < phytomer->petiole_length.size(); petiole++) {
478
479 output_xml << "\t\t\t\t\t<petiole>" << std::endl;
480 output_xml << "\t\t\t\t\t\t<petiole_length>" << phytomer->petiole_length.at(petiole) << "</petiole_length>" << std::endl;
481 output_xml << "\t\t\t\t\t\t<petiole_radius>" << phytomer->petiole_radii.at(petiole).front() << "</petiole_radius>" << std::endl;
482 output_xml << "\t\t\t\t\t\t<petiole_pitch>" << rad2deg(phytomer->petiole_pitch.at(petiole)) << "</petiole_pitch>" << std::endl;
483 output_xml << "\t\t\t\t\t\t<petiole_curvature>" << phytomer->petiole_curvature.at(petiole) << "</petiole_curvature>" << std::endl;
484 if (phytomer->leaf_rotation.at(petiole).size() == 1) { // not compound leaf
485 output_xml << "\t\t\t\t\t\t<leaflet_scale>" << 1.0 << "</leaflet_scale>" << std::endl;
486 } else {
487 float tip_ind = floor(float(phytomer->leaf_rotation.at(petiole).size() - 1) / 2.f);
488 output_xml << "\t\t\t\t\t\t<leaflet_scale>" << phytomer->leaf_size_max.at(petiole).at(int(tip_ind - 1)) / max(phytomer->leaf_size_max.at(petiole)) << "</leaflet_scale>" << std::endl;
489 }
490
491 for (uint leaf = 0; leaf < phytomer->leaf_rotation.at(petiole).size(); leaf++) {
492 output_xml << "\t\t\t\t\t\t<leaf>" << std::endl;
493 output_xml << "\t\t\t\t\t\t\t<leaf_scale>" << phytomer->leaf_size_max.at(petiole).at(leaf) * phytomer->current_leaf_scale_factor.at(petiole) << "</leaf_scale>" << std::endl;
494 output_xml << "\t\t\t\t\t\t\t<leaf_pitch>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).pitch) << "</leaf_pitch>" << std::endl;
495 output_xml << "\t\t\t\t\t\t\t<leaf_yaw>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).yaw) << "</leaf_yaw>" << std::endl;
496 output_xml << "\t\t\t\t\t\t\t<leaf_roll>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).roll) << "</leaf_roll>" << std::endl;
497 output_xml << "\t\t\t\t\t\t</leaf>" << std::endl;
498 }
499
500 output_xml << "\t\t\t\t\t</petiole>" << std::endl;
501 }
502 output_xml << "\t\t\t\t</internode>" << std::endl;
503 output_xml << "\t\t\t</phytomer>" << std::endl;
504 }
505 output_xml << "\t\t</shoot>" << std::endl;
506 }
507 output_xml << "\t</plant_instance>" << std::endl;
508 output_xml << "</helios>" << std::endl;
509 output_xml.close();
510}
511
512void PlantArchitecture::writeQSMCylinderFile(uint plantID, const std::string &filename) const {
513
514 if (plant_instances.find(plantID) == plant_instances.end()) {
515 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Plant ID " + std::to_string(plantID) + " does not exist.");
516 }
517
518 std::string output_file = filename;
519 if (!validateOutputPath(output_file, {".txt", ".TXT"})) {
520 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
521 } else if (getFileName(output_file).empty()) {
522 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): The output file given was a directory. This argument should be the path to a file not to a directory.");
523 }
524
525 std::ofstream output_qsm(filename);
526
527 if (!output_qsm.is_open()) {
528 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
529 }
530
531 // Write header line
532 output_qsm << "radius (m)\tlength (m)\tstart_point\taxis_direction\tparent\textension\tbranch\tbranch_order\tposition_in_branch\tmad\tSurfCov\tadded\tUnmodRadius (m)" << std::endl;
533
534 const auto &plant = plant_instances.at(plantID);
535
536 // Cylinder ID counter (row number = cylinder ID in TreeQSM format)
537 uint cylinder_id = 1;
538
539 // Maps to track relationships between shoots and cylinders
540 std::map<int, std::vector<uint>> shoot_cylinder_ids; // shoot ID -> cylinder IDs
541 std::map<int, uint> shoot_branch_id; // shoot ID -> branch ID
542 std::map<int, uint> shoot_branch_order; // shoot ID -> branch order
543
544 // Assign branch IDs and orders to shoots
545 uint branch_id_counter = 1;
546 for (const auto &shoot: plant.shoot_tree) {
547 shoot_branch_id[shoot->ID] = branch_id_counter++;
548
549 // Determine branch order based on parent
550 if (shoot->parent_shoot_ID == -1) {
551 // Base shoot is order 0 (trunk)
552 shoot_branch_order[shoot->ID] = 0;
553 } else {
554 // Child shoot has order = parent order + 1
555 shoot_branch_order[shoot->ID] = shoot_branch_order[shoot->parent_shoot_ID] + 1;
556 }
557 }
558
559 // Process each shoot
560 for (const auto &shoot: plant.shoot_tree) {
561
562 // Get shoot properties
563 uint branch_id = shoot_branch_id[shoot->ID];
564 uint branch_order = shoot_branch_order[shoot->ID];
565 uint position_in_branch = 1;
566 // Track the last vertex position for vertex sharing between phytomers
567 helios::vec3 last_vertex_position;
568 bool has_last_vertex = false;
569
570 // Process each phytomer in the shoot
571 for (uint phytomer_idx = 0; phytomer_idx < shoot->phytomers.size(); phytomer_idx++) {
572
573 const auto &vertices = shoot->shoot_internode_vertices[phytomer_idx];
574 const auto &radii = shoot->shoot_internode_radii[phytomer_idx];
575
576 // Handle vertex sharing for single-segment phytomer tubes
577 if (vertices.size() == 1 && has_last_vertex) {
578 // This phytomer has only one vertex - use the last vertex from previous phytomer as start
579 helios::vec3 start = last_vertex_position;
580 helios::vec3 current_end = vertices[0];
581 float current_radius = radii[0];
582
583 // Calculate initial length
584 helios::vec3 axis = current_end - start;
585 float length = axis.magnitude();
586
587 axis = axis / length; // Normalize axis
588
589 // Process this as a single cylinder
590 // Determine parent cylinder ID
591 uint parent_id = 0;
592 if (cylinder_id > 1) {
593 parent_id = cylinder_id - 1; // Parent is previous cylinder
594 }
595
596 // Extension cylinder (next cylinder in same branch) - will be updated later if needed
597 uint extension_id = 0;
598
599 // Write cylinder data
600 output_qsm << std::fixed << std::setprecision(4);
601 output_qsm << current_radius << "\t";
602 output_qsm << length << "\t";
603 output_qsm << start.x << "\t" << start.y << "\t" << start.z << "\t";
604 output_qsm << axis.x << "\t" << axis.y << "\t" << axis.z << "\t";
605 output_qsm << parent_id << "\t";
606 output_qsm << extension_id << "\t";
607 output_qsm << branch_id << "\t";
608 output_qsm << branch_order << "\t";
609 output_qsm << position_in_branch << "\t";
610 output_qsm << "0.0002" << "\t"; // mad (using default value)
611 output_qsm << "1" << "\t"; // SurfCov (using default value)
612 output_qsm << "0" << "\t"; // added flag
613 output_qsm << current_radius << std::endl; // UnmodRadius
614
615 // Store cylinder ID for this shoot
616 shoot_cylinder_ids[shoot->ID].push_back(cylinder_id);
617
618 cylinder_id++;
619 position_in_branch++;
620
621 // Update last vertex for next phytomer
622 last_vertex_position = current_end;
623
624 } else {
625 // Normal processing for phytomers with multiple vertices
626 for (int seg = 0; seg < vertices.size() - 1; seg++) {
627
628 // Start with the current segment
629 helios::vec3 start = vertices[seg];
630 helios::vec3 current_end = vertices[seg + 1];
631 float current_radius = radii[seg];
632
633 // Calculate initial length
634 helios::vec3 axis = current_end - start;
635 float length = axis.magnitude();
636
637 axis = axis / length; // Normalize axis
638
639 // Determine parent cylinder ID
640 uint parent_id = 0;
641 if (cylinder_id > 1) {
642 if (seg == 0 && phytomer_idx == 0 && shoot->parent_shoot_ID != -1) {
643 // First cylinder of child shoot - parent is last cylinder of connection point
644 // For simplicity, using previous cylinder as parent
645 parent_id = cylinder_id - 1;
646 } else if (seg == 0 && phytomer_idx > 0) {
647 // First segment of new phytomer - parent is last segment of previous phytomer
648 parent_id = cylinder_id - 1;
649 } else {
650 // Continuation within phytomer - parent is previous segment
651 parent_id = cylinder_id - 1;
652 }
653 }
654
655 // Extension cylinder (next cylinder in same branch) - will be updated later if needed
656 uint extension_id = 0;
657
658 // Write cylinder data
659 output_qsm << std::fixed << std::setprecision(4);
660 output_qsm << current_radius << "\t";
661 output_qsm << length << "\t";
662 output_qsm << start.x << "\t" << start.y << "\t" << start.z << "\t";
663 output_qsm << axis.x << "\t" << axis.y << "\t" << axis.z << "\t";
664 output_qsm << parent_id << "\t";
665 output_qsm << extension_id << "\t";
666 output_qsm << branch_id << "\t";
667 output_qsm << branch_order << "\t";
668 output_qsm << position_in_branch << "\t";
669 output_qsm << "0.0002" << "\t"; // mad (using default value)
670 output_qsm << "1" << "\t"; // SurfCov (using default value)
671 output_qsm << "0" << "\t"; // added flag
672 output_qsm << current_radius << std::endl; // UnmodRadius
673
674 // Store cylinder ID for this shoot
675 shoot_cylinder_ids[shoot->ID].push_back(cylinder_id);
676
677 cylinder_id++;
678 position_in_branch++;
679 }
680
681 // Update last vertex position for vertex sharing
682 if (vertices.size() >= 2) {
683 last_vertex_position = vertices.back();
684 has_last_vertex = true;
685 } else if (vertices.size() == 1) {
686 // This shouldn't happen in normal processing, but handle it for completeness
687 last_vertex_position = vertices[0];
688 has_last_vertex = true;
689 }
690 }
691 }
692 }
693
694 output_qsm.close();
695}
696
697std::vector<uint> PlantArchitecture::readPlantStructureXML(const std::string &filename, bool quiet) {
698
699 if (!quiet) {
700 std::cout << "Loading plant architecture XML file: " << filename << "..." << std::flush;
701 }
702
703 std::string fn = filename;
704 std::string ext = getFileExtension(filename);
705 if (ext != ".xml" && ext != ".XML") {
706 helios_runtime_error("failed.\n File " + fn + " is not XML format.");
707 }
708
709 std::vector<uint> plantIDs;
710
711 // Using "pugixml" parser. See pugixml.org
712 pugi::xml_document xmldoc;
713
714 // Resolve file path using project-based resolution
715 std::filesystem::path resolved_path = resolveProjectFile(filename);
716 std::string resolved_filename = resolved_path.string();
717
718 // load file
719 pugi::xml_parse_result load_result = xmldoc.load_file(resolved_filename.c_str());
720
721 // error checking
722 if (!load_result) {
723 helios_runtime_error("ERROR (Context::readPlantStructureXML): Could not parse " + std::string(filename) + ":\nError description: " + load_result.description());
724 }
725
726 pugi::xml_node helios = xmldoc.child("helios");
727
728 pugi::xml_node node;
729 std::string node_string;
730
731 if (helios.empty()) {
732 if (!quiet) {
733 std::cout << "failed." << std::endl;
734 }
735 helios_runtime_error("ERROR (Context::readPlantStructureXML): XML file must have tag '<helios> ... </helios>' bounding all other tags.");
736 }
737
738 size_t phytomer_count = 0;
739
740 std::map<int, int> shoot_ID_mapping;
741
742 for (pugi::xml_node plant = helios.child("plant_instance"); plant; plant = plant.next_sibling("plant_instance")) {
743
744 int plantID = std::stoi(plant.attribute("ID").value());
745
746 // base position
747 node_string = "base_position";
748 vec3 base_position = parse_xml_tag_vec3(plant.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
749
750 // plant age
751 node_string = "plant_age";
752 float plant_age = parse_xml_tag_float(plant.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
753
754 plantID = addPlantInstance(base_position, plant_age);
755 plantIDs.push_back(plantID);
756
757 int current_shoot_ID;
758
759 for (pugi::xml_node shoot = plant.child("shoot"); shoot; shoot = shoot.next_sibling("shoot")) {
760
761 int shootID = std::stoi(shoot.attribute("ID").value());
762 bool base_shoot = true;
763
764 // shoot type
765 node_string = "shoot_type_label";
766 std::string shoot_type_label = parse_xml_tag_string(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
767
768 // parent shoot ID
769 node_string = "parent_shoot_ID";
770 int parent_shoot_ID = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
771
772 // parent node index
773 node_string = "parent_node_index";
774 int parent_node_index = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
775
776 // parent petiole index
777 node_string = "parent_petiole_index";
778 int parent_petiole_index = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
779
780 // base rotation
781 node_string = "base_rotation";
782 vec3 base_rot = parse_xml_tag_vec3(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
783 AxisRotation base_rotation(deg2rad(base_rot.x), deg2rad(base_rot.y), deg2rad(base_rot.z));
784
785 for (pugi::xml_node phytomer = shoot.child("phytomer"); phytomer; phytomer = phytomer.next_sibling("phytomer")) {
786
787 pugi::xml_node internode = phytomer.child("internode");
788
789 // internode length
790 node_string = "internode_length";
791 float internode_length = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
792
793 // internode radius
794 node_string = "internode_radius";
795 float internode_radius = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
796
797 // internode pitch
798 node_string = "internode_pitch";
799 float internode_pitch = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
800
801 // internode phyllotactic angle
802 node_string = "internode_phyllotactic_angle";
803 float internode_phyllotactic_angle = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
804
805 float petiole_length;
806 float petiole_radius;
807 float petiole_pitch;
808 float petiole_curvature;
809 float leaflet_scale;
810 std::vector<std::vector<float>> leaf_scale; // first index is petiole within internode; second index is leaf within petiole
811 std::vector<std::vector<float>> leaf_pitch;
812 std::vector<std::vector<float>> leaf_yaw;
813 std::vector<std::vector<float>> leaf_roll;
814 for (pugi::xml_node petiole = internode.child("petiole"); petiole; petiole = petiole.next_sibling("petiole")) {
815
816 // petiole length
817 node_string = "petiole_length";
818 petiole_length = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
819
820 // petiole radius
821 node_string = "petiole_radius";
822 petiole_radius = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
823
824 // petiole pitch
825 node_string = "petiole_pitch";
826 petiole_pitch = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
827
828 // petiole curvature
829 node_string = "petiole_curvature";
830 petiole_curvature = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
831
832 // leaflet scale factor
833 node_string = "leaflet_scale";
834 leaflet_scale = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
835
836 leaf_scale.resize(leaf_scale.size() + 1);
837 leaf_pitch.resize(leaf_pitch.size() + 1);
838 leaf_yaw.resize(leaf_yaw.size() + 1);
839 leaf_roll.resize(leaf_roll.size() + 1);
840 for (pugi::xml_node leaf = petiole.child("leaf"); leaf; leaf = leaf.next_sibling("leaf")) {
841
842 // leaf scale factor
843 node_string = "leaf_scale";
844 leaf_scale.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
845
846 // leaf pitch
847 node_string = "leaf_pitch";
848 leaf_pitch.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
849
850 // leaf yaw
851 node_string = "leaf_yaw";
852 leaf_yaw.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
853
854 // leaf roll
855 node_string = "leaf_roll";
856 leaf_roll.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
857 }
858 } // petioles
859
860 if (shoot_types.find(shoot_type_label) == shoot_types.end()) {
861 helios_runtime_error("ERROR (PlantArchitecture::readPlantStructureXML): Shoot type " + shoot_type_label + " not found in shoot types.");
862 }
863
864
865 ShootParameters shoot_parameters = getCurrentShootParameters(shoot_type_label);
866
867 shoot_parameters.phytomer_parameters.phytomer_creation_function = nullptr;
868
869 shoot_parameters.phytomer_parameters.internode.pitch = internode_pitch;
870 shoot_parameters.phytomer_parameters.internode.phyllotactic_angle = internode_phyllotactic_angle;
871
872 shoot_parameters.phytomer_parameters.petiole.length = petiole_length;
873 shoot_parameters.phytomer_parameters.petiole.radius = petiole_radius;
874 shoot_parameters.phytomer_parameters.petiole.pitch = petiole_pitch;
875 shoot_parameters.phytomer_parameters.petiole.curvature = petiole_curvature;
876
877 shoot_parameters.phytomer_parameters.leaf.prototype_scale = 1.f; // leaf_scale.front().at(tip_ind);
878 shoot_parameters.phytomer_parameters.leaf.pitch = 0;
879 shoot_parameters.phytomer_parameters.leaf.yaw = 0;
880 shoot_parameters.phytomer_parameters.leaf.roll = 0;
881 shoot_parameters.phytomer_parameters.leaf.leaflet_scale = leaflet_scale;
882
883 std::string shoot_label = "shoot_" + std::to_string(phytomer_count);
884 defineShootType(shoot_label, shoot_parameters);
885
886 if (base_shoot) {
887
888 if (parent_shoot_ID < 0) { // this is the first shoot of the plant
889 current_shoot_ID = addBaseStemShoot(plantID, 1, base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, shoot_label);
890 shoot_ID_mapping[shootID] = current_shoot_ID;
891 } else { // this is a child of an existing shoot
892 current_shoot_ID = addChildShoot(plantID, shoot_ID_mapping.at(parent_shoot_ID), parent_node_index, 1, base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, shoot_label, parent_petiole_index);
893 shoot_ID_mapping[shootID] = current_shoot_ID;
894 }
895
896 base_shoot = false;
897 } else {
898 appendPhytomerToShoot(plantID, current_shoot_ID, shoot_parameters.phytomer_parameters, internode_radius, internode_length, 1, 1);
899 }
900
901 // rotate and scale leaves
902 assert(leaf_scale.size() == leaf_pitch.size());
903 auto phytomer_ptr = plant_instances.at(plantID).shoot_tree.at(current_shoot_ID)->phytomers.back();
904 for (int petiole = 0; petiole < leaf_scale.size(); petiole++) {
905
906 float tip_ind = floor(float(leaf_scale.at(petiole).size() - 1) / 2.f);
907 phytomer_ptr->setLeafPrototypeScale(petiole, leaf_scale.at(petiole).at(tip_ind));
908
909 phytomer_ptr->scaleLeafPrototypeScale(petiole, 5);
910
911 for (int leaf = 0; leaf < phytomer_ptr->leaf_rotation.at(petiole).size(); leaf++) {
912 phytomer_ptr->rotateLeaf(petiole, leaf, make_AxisRotation(deg2rad(leaf_pitch.at(petiole).at(leaf)), deg2rad(leaf_yaw.at(petiole).at(leaf)), deg2rad(-leaf_roll.at(petiole).at(leaf))));
913 }
914 }
915
916 phytomer_count++;
917 } // phytomers
918
919 } // shoots
920
921 } // plant instances
922
923 if (!quiet) {
924 std::cout << "done." << std::endl;
925 }
926 return plantIDs;
927}