Skip to content

Commit ec7d191

Browse files
committed
[1.3.55] 2025-10-18
## Core - Added `Context::setObjectDataFromPrimitiveDataMean()` method to calculate the mean of primitive data values across all child primitives of an object and set it as object data. Supports float, double, vec2, vec3, and vec4 data types. Only primitives with the specified data label are included in the calculation. - Improved XML formatting for vector global data types (vec2, vec3, vec4, int2, int3, int4) - now writes each value on a separate line with proper indentation for better readability instead of all values on one line ## Visualizer - Fixed issue with navigation gizmo where it could be blocked by objects extending past the camera near plane by moving it closer to the near plane (z-position changed from 0.01 to -0.9999) ## Leaf Optics - Added `LeafOptics::getPropertiesFromSpectrum()` methods to retrieve PROSPECT model parameters from primitives based on their assigned reflectivity spectra. Available as both single primitive and vector overloads. The method queries primitives for their "reflectivity_spectrum" data and assigns the corresponding PROSPECT parameters (chlorophyll, carotenoid, anthocyanin, water, etc.) as primitive data if the spectrum matches one generated by the LeafOptics instance. ## Plant Architecture - Fixed a bug when `petioles_per_internode` is set to 0, which caused child shoot insertion angles to be incorrect. This caused major issues in the weed models "puncturevine" and "bindweed" - Fixed a bug where internodes that start out extremely small were only being scaled in the radius but not length. This caused major issues in the weed model "cheeseweed" - Improved `ShootParameters` documentation comments for `insertion_angle_tip` and `insertion_angle_decay_rate` for clarity ## Radiation - Cleaned up camera_spectral_library.xml by removing duplicate copyright comments for Canon 20D, Nikon D700, Nikon D50, and SONY NEX-5N camera spectral data
1 parent baefe70 commit ec7d191

File tree

17 files changed

+855
-110
lines changed

17 files changed

+855
-110
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ Helios is a C++ library for 3D physical simulation of plant and environmental sy
88

99
In order to build and compile the core library, you will need to install a C/C++ compiler (recommended are the GNU C compilers version 7.0+), and CMake. In order to run many of the model plug-ins, you will need to install NVIDIA CUDA 10.2+, and a GPU with compute capability 5.0+. The software has been tested on Linux, Mac, and Windows platforms. The YouTube channel linked above has a number of tutorials for getting started.
1010

11+
**NEW** : Helios now has a Python API! Please see the [PyHelios API documentation](https://plantsimulationlab.github.io/PyHelios/) for more information.
12+
13+
<div align="center">
14+
<img src="https://raw.githubusercontent.com/PlantSimulationLab/PyHelios/master/docs/images/PyHelios_logo_whiteborder.png" alt="" width="100" />
15+
</div>
16+
1117
![Almond Reconstruction](doc/images/AlmondVarietyReconstruction.png)

core/include/Context.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4221,6 +4221,16 @@ namespace helios {
42214221
}
42224222
}
42234223

4224+
//! Set object data from mean of primitive data values for all child primitives
4225+
/**
4226+
* \param[in] objID Unique identifier of compound object
4227+
* \param[in] label Name of primitive data to aggregate and resulting object data label
4228+
* \note The data type is automatically detected from the primitive data (supports float, double, vec2, vec3, vec4)
4229+
* \note Only primitives that have the specified primitive data are included in the calculation
4230+
* \note Raises error if no primitives have the specified primitive data
4231+
*/
4232+
void setObjectDataFromPrimitiveDataMean(uint objID, const std::string &label);
4233+
42244234
//! Get data value associated with a compound object
42254235
/**
42264236
* \tparam T Object data type

core/src/Context_data.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,127 @@ void Context::calculatePrimitiveDataMean(const std::vector<uint> &UUIDs, const s
537537
}
538538
}
539539

540+
void Context::setObjectDataFromPrimitiveDataMean(uint objID, const std::string &label) {
541+
#ifdef HELIOS_DEBUG
542+
if (!doesObjectExist(objID)) {
543+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): Object ID of " + std::to_string(objID) + " does not exist in the Context.");
544+
}
545+
#endif
546+
547+
// Get primitive UUIDs for this object
548+
std::vector<uint> UUIDs = getObjectPrimitiveUUIDs(objID);
549+
550+
if (UUIDs.empty()) {
551+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): Object ID " + std::to_string(objID) + " has no primitive children.");
552+
}
553+
554+
// Determine the data type by checking the first primitive that has this data
555+
HeliosDataType data_type = HELIOS_TYPE_UNKNOWN;
556+
for (uint UUID : UUIDs) {
557+
if (doesPrimitiveExist(UUID) && doesPrimitiveDataExist(UUID, label.c_str())) {
558+
data_type = getPrimitiveDataType(label.c_str());
559+
break;
560+
}
561+
}
562+
563+
if (data_type == HELIOS_TYPE_UNKNOWN) {
564+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): Primitive data '" + label + "' does not exist for any primitives in object " + std::to_string(objID) + ".");
565+
}
566+
567+
// Validate data type is supported for mean calculation
568+
if (data_type != HELIOS_TYPE_FLOAT && data_type != HELIOS_TYPE_DOUBLE &&
569+
data_type != HELIOS_TYPE_VEC2 && data_type != HELIOS_TYPE_VEC3 && data_type != HELIOS_TYPE_VEC4) {
570+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): Cannot calculate mean for primitive data type. Only float, double, vec2, vec3, and vec4 are supported.");
571+
}
572+
573+
// Calculate mean based on data type
574+
if (data_type == HELIOS_TYPE_FLOAT) {
575+
float value;
576+
float sum = 0.f;
577+
size_t count = 0;
578+
for (uint UUID : UUIDs) {
579+
if (doesPrimitiveExist(UUID) && doesPrimitiveDataExist(UUID, label.c_str())) {
580+
getPrimitiveData(UUID, label.c_str(), value);
581+
sum += value;
582+
count++;
583+
}
584+
}
585+
if (count == 0) {
586+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): No primitives in object " + std::to_string(objID) + " have primitive data '" + label + "'.");
587+
}
588+
float mean = sum / float(count);
589+
setObjectData(objID, label.c_str(), mean);
590+
591+
} else if (data_type == HELIOS_TYPE_DOUBLE) {
592+
double value;
593+
double sum = 0.0;
594+
size_t count = 0;
595+
for (uint UUID : UUIDs) {
596+
if (doesPrimitiveExist(UUID) && doesPrimitiveDataExist(UUID, label.c_str())) {
597+
getPrimitiveData(UUID, label.c_str(), value);
598+
sum += value;
599+
count++;
600+
}
601+
}
602+
if (count == 0) {
603+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): No primitives in object " + std::to_string(objID) + " have primitive data '" + label + "'.");
604+
}
605+
double mean = sum / double(count);
606+
setObjectData(objID, label.c_str(), mean);
607+
608+
} else if (data_type == HELIOS_TYPE_VEC2) {
609+
vec2 value;
610+
vec2 sum(0.f, 0.f);
611+
size_t count = 0;
612+
for (uint UUID : UUIDs) {
613+
if (doesPrimitiveExist(UUID) && doesPrimitiveDataExist(UUID, label.c_str())) {
614+
getPrimitiveData(UUID, label.c_str(), value);
615+
sum = sum + value;
616+
count++;
617+
}
618+
}
619+
if (count == 0) {
620+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): No primitives in object " + std::to_string(objID) + " have primitive data '" + label + "'.");
621+
}
622+
vec2 mean = sum / float(count);
623+
setObjectData(objID, label.c_str(), mean);
624+
625+
} else if (data_type == HELIOS_TYPE_VEC3) {
626+
vec3 value;
627+
vec3 sum(0.f, 0.f, 0.f);
628+
size_t count = 0;
629+
for (uint UUID : UUIDs) {
630+
if (doesPrimitiveExist(UUID) && doesPrimitiveDataExist(UUID, label.c_str())) {
631+
getPrimitiveData(UUID, label.c_str(), value);
632+
sum = sum + value;
633+
count++;
634+
}
635+
}
636+
if (count == 0) {
637+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): No primitives in object " + std::to_string(objID) + " have primitive data '" + label + "'.");
638+
}
639+
vec3 mean = sum / float(count);
640+
setObjectData(objID, label.c_str(), mean);
641+
642+
} else if (data_type == HELIOS_TYPE_VEC4) {
643+
vec4 value;
644+
vec4 sum(0.f, 0.f, 0.f, 0.f);
645+
size_t count = 0;
646+
for (uint UUID : UUIDs) {
647+
if (doesPrimitiveExist(UUID) && doesPrimitiveDataExist(UUID, label.c_str())) {
648+
getPrimitiveData(UUID, label.c_str(), value);
649+
sum = sum + value;
650+
count++;
651+
}
652+
}
653+
if (count == 0) {
654+
helios_runtime_error("ERROR (Context::setObjectDataFromPrimitiveDataMean): No primitives in object " + std::to_string(objID) + " have primitive data '" + label + "'.");
655+
}
656+
vec4 mean = sum / float(count);
657+
setObjectData(objID, label.c_str(), mean);
658+
}
659+
}
660+
540661
void Context::calculatePrimitiveDataAreaWeightedMean(const std::vector<uint> &UUIDs, const std::string &label, float &awt_mean) const {
541662
float value, A;
542663
float sum = 0.f;

core/src/Context_fileIO.cpp

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3082,59 +3082,41 @@ void Context::writeXML(const char *filename, const std::vector<uint> &UUIDs, boo
30823082
}
30833083
outfile << "</globaldata_double>" << std::endl;
30843084
} else if (type == HELIOS_TYPE_VEC2) {
3085-
outfile << " <globaldata_vec2 label=\"" << label << "\">" << std::flush;
3085+
outfile << " <globaldata_vec2 label=\"" << label << "\">" << std::endl;
30863086
for (size_t i = 0; i < data.size; i++) {
3087-
outfile << data.global_data_vec2.at(i).x << " " << data.global_data_vec2.at(i).y << std::flush;
3088-
if (i != data.size - 1) {
3089-
outfile << " " << std::flush;
3090-
}
3087+
outfile << " " << data.global_data_vec2.at(i).x << " " << data.global_data_vec2.at(i).y << std::endl;
30913088
}
3092-
outfile << "</globaldata_vec2>" << std::endl;
3089+
outfile << " </globaldata_vec2>" << std::endl;
30933090
} else if (type == HELIOS_TYPE_VEC3) {
3094-
outfile << " <globaldata_vec3 label=\"" << label << "\">" << std::flush;
3091+
outfile << " <globaldata_vec3 label=\"" << label << "\">" << std::endl;
30953092
for (size_t i = 0; i < data.size; i++) {
3096-
outfile << data.global_data_vec3.at(i).x << " " << data.global_data_vec3.at(i).y << " " << data.global_data_vec3.at(i).z << std::flush;
3097-
if (i != data.size - 1) {
3098-
outfile << " " << std::flush;
3099-
}
3093+
outfile << " " << data.global_data_vec3.at(i).x << " " << data.global_data_vec3.at(i).y << " " << data.global_data_vec3.at(i).z << std::endl;
31003094
}
3101-
outfile << "</globaldata_vec3>" << std::endl;
3095+
outfile << " </globaldata_vec3>" << std::endl;
31023096
} else if (type == HELIOS_TYPE_VEC4) {
3103-
outfile << " <globaldata_vec4 label=\"" << label << "\">" << std::flush;
3097+
outfile << " <globaldata_vec4 label=\"" << label << "\">" << std::endl;
31043098
for (size_t i = 0; i < data.size; i++) {
3105-
outfile << data.global_data_vec4.at(i).x << " " << data.global_data_vec4.at(i).y << " " << data.global_data_vec4.at(i).z << " " << data.global_data_vec4.at(i).w << std::flush;
3106-
if (i != data.size - 1) {
3107-
outfile << " " << std::flush;
3108-
}
3099+
outfile << " " << data.global_data_vec4.at(i).x << " " << data.global_data_vec4.at(i).y << " " << data.global_data_vec4.at(i).z << " " << data.global_data_vec4.at(i).w << std::endl;
31093100
}
3110-
outfile << "</globaldata_vec4>" << std::endl;
3101+
outfile << " </globaldata_vec4>" << std::endl;
31113102
} else if (type == HELIOS_TYPE_INT2) {
3112-
outfile << " <globaldata_int2 label=\"" << label << "\">" << std::flush;
3103+
outfile << " <globaldata_int2 label=\"" << label << "\">" << std::endl;
31133104
for (size_t i = 0; i < data.size; i++) {
3114-
outfile << data.global_data_int2.at(i).x << " " << data.global_data_int2.at(i).y << std::flush;
3115-
if (i != data.size - 1) {
3116-
outfile << " " << std::flush;
3117-
}
3105+
outfile << " " << data.global_data_int2.at(i).x << " " << data.global_data_int2.at(i).y << std::endl;
31183106
}
3119-
outfile << "</globaldata_int2>" << std::endl;
3107+
outfile << " </globaldata_int2>" << std::endl;
31203108
} else if (type == HELIOS_TYPE_INT3) {
3121-
outfile << " <globaldata_int3 label=\"" << label << "\">" << std::flush;
3109+
outfile << " <globaldata_int3 label=\"" << label << "\">" << std::endl;
31223110
for (size_t i = 0; i < data.size; i++) {
3123-
outfile << data.global_data_int3.at(i).x << " " << data.global_data_int3.at(i).y << data.global_data_int3.at(i).z << std::flush;
3124-
if (i != data.size - 1) {
3125-
outfile << " " << std::flush;
3126-
}
3111+
outfile << " " << data.global_data_int3.at(i).x << " " << data.global_data_int3.at(i).y << " " << data.global_data_int3.at(i).z << std::endl;
31273112
}
3128-
outfile << "</globaldata_int3>" << std::endl;
3113+
outfile << " </globaldata_int3>" << std::endl;
31293114
} else if (type == HELIOS_TYPE_INT4) {
3130-
outfile << " <globaldata_int4 label=\"" << label << "\">" << std::flush;
3115+
outfile << " <globaldata_int4 label=\"" << label << "\">" << std::endl;
31313116
for (size_t i = 0; i < data.size; i++) {
3132-
outfile << data.global_data_int4.at(i).x << " " << data.global_data_int4.at(i).y << data.global_data_int4.at(i).z << data.global_data_int4.at(i).w << std::flush;
3133-
if (i != data.size - 1) {
3134-
outfile << " " << std::flush;
3135-
}
3117+
outfile << " " << data.global_data_int4.at(i).x << " " << data.global_data_int4.at(i).y << " " << data.global_data_int4.at(i).z << " " << data.global_data_int4.at(i).w << std::endl;
31363118
}
3137-
outfile << "</globaldata_int4>" << std::endl;
3119+
outfile << " </globaldata_int4>" << std::endl;
31383120
} else if (type == HELIOS_TYPE_STRING) {
31393121
outfile << " <globaldata_string label=\"" << label << "\">" << std::flush;
31403122
for (size_t i = 0; i < data.size; i++) {

core/tests/Test_XML.h

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,5 +291,76 @@ TEST_CASE("Context XML I/O Functions") {
291291
std::remove(test_file);
292292
}
293293

294+
SUBCASE("vector global data XML formatting") {
295+
Context ctx;
296+
297+
// Add various vector type global data
298+
std::vector<vec2> vec2_data = {make_vec2(400, 0.0666747f), make_vec2(401, 0.0640556f), make_vec2(402, 0.0619332f)};
299+
std::vector<vec3> vec3_data = {make_vec3(1.1f, 2.2f, 3.3f), make_vec3(4.4f, 5.5f, 6.6f)};
300+
std::vector<vec4> vec4_data = {make_vec4(1.1f, 2.2f, 3.3f, 4.4f), make_vec4(5.5f, 6.6f, 7.7f, 8.8f)};
301+
std::vector<int2> int2_data = {make_int2(1, 2), make_int2(3, 4)};
302+
std::vector<int3> int3_data = {make_int3(1, 2, 3), make_int3(4, 5, 6)};
303+
std::vector<int4> int4_data = {make_int4(1, 2, 3, 4), make_int4(5, 6, 7, 8)};
304+
305+
ctx.setGlobalData("test_vec2", vec2_data);
306+
ctx.setGlobalData("test_vec3", vec3_data);
307+
ctx.setGlobalData("test_vec4", vec4_data);
308+
ctx.setGlobalData("test_int2", int2_data);
309+
ctx.setGlobalData("test_int3", int3_data);
310+
ctx.setGlobalData("test_int4", int4_data);
311+
312+
const char *test_file = "helios_vector_test.xml";
313+
ctx.writeXML(test_file, true);
314+
315+
// Load and verify all vector data
316+
Context ctx2;
317+
ctx2.loadXML(test_file, true);
318+
319+
// Verify vec2
320+
std::vector<vec2> loaded_vec2;
321+
ctx2.getGlobalData("test_vec2", loaded_vec2);
322+
DOCTEST_CHECK(loaded_vec2.size() == 3);
323+
DOCTEST_CHECK(loaded_vec2[0].x == doctest::Approx(400.0f));
324+
DOCTEST_CHECK(loaded_vec2[0].y == doctest::Approx(0.0666747f));
325+
DOCTEST_CHECK(loaded_vec2[2].x == doctest::Approx(402.0f));
326+
327+
// Verify vec3
328+
std::vector<vec3> loaded_vec3;
329+
ctx2.getGlobalData("test_vec3", loaded_vec3);
330+
DOCTEST_CHECK(loaded_vec3.size() == 2);
331+
DOCTEST_CHECK(loaded_vec3[0] == vec3(1.1f, 2.2f, 3.3f));
332+
DOCTEST_CHECK(loaded_vec3[1] == vec3(4.4f, 5.5f, 6.6f));
333+
334+
// Verify vec4
335+
std::vector<vec4> loaded_vec4;
336+
ctx2.getGlobalData("test_vec4", loaded_vec4);
337+
DOCTEST_CHECK(loaded_vec4.size() == 2);
338+
DOCTEST_CHECK(loaded_vec4[0] == vec4(1.1f, 2.2f, 3.3f, 4.4f));
339+
DOCTEST_CHECK(loaded_vec4[1] == vec4(5.5f, 6.6f, 7.7f, 8.8f));
340+
341+
// Verify int2
342+
std::vector<int2> loaded_int2;
343+
ctx2.getGlobalData("test_int2", loaded_int2);
344+
DOCTEST_CHECK(loaded_int2.size() == 2);
345+
DOCTEST_CHECK(loaded_int2[0] == int2(1, 2));
346+
DOCTEST_CHECK(loaded_int2[1] == int2(3, 4));
347+
348+
// Verify int3
349+
std::vector<int3> loaded_int3;
350+
ctx2.getGlobalData("test_int3", loaded_int3);
351+
DOCTEST_CHECK(loaded_int3.size() == 2);
352+
DOCTEST_CHECK(loaded_int3[0] == int3(1, 2, 3));
353+
DOCTEST_CHECK(loaded_int3[1] == int3(4, 5, 6));
354+
355+
// Verify int4
356+
std::vector<int4> loaded_int4;
357+
ctx2.getGlobalData("test_int4", loaded_int4);
358+
DOCTEST_CHECK(loaded_int4.size() == 2);
359+
DOCTEST_CHECK(loaded_int4[0] == int4(1, 2, 3, 4));
360+
DOCTEST_CHECK(loaded_int4[1] == int4(5, 6, 7, 8));
361+
362+
std::remove(test_file);
363+
}
364+
294365
// Note: scanXMLForTag testing removed due to complex XML tag structure requirements
295366
}

0 commit comments

Comments
 (0)