From 8c3dd88b7da5fdd822ca294a3614d7e2ca9fd39f Mon Sep 17 00:00:00 2001 From: Robert Chisholm Date: Thu, 23 Nov 2023 14:50:48 +0000 Subject: [PATCH] Adds RunPlanVector::setPropertyStep() This has behaviour similar to Python's range(), more appropriate than lerp in many cases with integers. Providing this will hopefully help people avoid using lerp wrongly for integers. --- include/flamegpu/simulation/RunPlanVector.h | 90 ++++++++++++++++++- swig/python/flamegpu.i | 1 + tests/python/simulation/test_RunPlanVector.py | 78 +++++++++++++++- .../simulation/test_RunPlanVector.cu | 77 +++++++++++++++- 4 files changed, 240 insertions(+), 6 deletions(-) diff --git a/include/flamegpu/simulation/RunPlanVector.h b/include/flamegpu/simulation/RunPlanVector.h index 818d27d2d..fd0b2bfd5 100644 --- a/include/flamegpu/simulation/RunPlanVector.h +++ b/include/flamegpu/simulation/RunPlanVector.h @@ -113,9 +113,11 @@ class RunPlanVector : private std::vector { * @throws exception::InvalidEnvProperty If a property of the name does not exist * @throws exception::InvalidEnvPropertyType If a property with the name has a type different to T, or length > 1 * @throws exception::OutOfBoundsException If this vector has a length less than 2 + * @see setPropertyLerpRange(const std::string &name, flamegpu::size_type, T min, T max) + * @see setPropertyStep(const std::string&, T, T) */ template - void setPropertyLerpRange(const std::string &name, const T min, const T max); + void setPropertyLerpRange(const std::string &name, T min, T max); /** * Array property element equivalent of setPropertyLerpRange() * Sweep element of named environment property array over an inclusive uniform distribution @@ -130,10 +132,43 @@ class RunPlanVector : private std::vector { * @throws exception::InvalidEnvPropertyType If a property with the name has a type different to T * @throws exception::OutOfBoundsException If index is greater than or equal to the length of the environment property array * @throws exception::OutOfBoundsException If this vector has a length less than 2 - * @see setPropertyUniformDistribution(const std::string &name, T min, T max) + * @see setPropertyLerpRange(const std::string &name, T min, T max) + * @see setPropertyStep(const std::string&, flamegpu::size_type, T, T) */ template - void setPropertyLerpRange(const std::string &name, const flamegpu::size_type index, const T min, const T max); + void setPropertyLerpRange(const std::string &name, flamegpu::size_type index, T min, T max); + /** + * Increment named environment property with a user defined step + * value = init + index * step + * @param name The name of the environment property to set + * @param init The value of the first environment property + * @param step The value to increment each subsequent environment property by + * @tparam T The type of the environment property, this must match the ModelDescription + * @throws exception::InvalidEnvProperty If a property of the name does not exist + * @throws exception::InvalidEnvPropertyType If a property with the name has a type different to T, or length > 1 + * @throws exception::OutOfBoundsException If this vector has a length less than 2 + * @see setPropertyStep(const std::string&, flamegpu::size_type, T, T) + * @see setPropertyLerpRange(const std::string&, T, T) + */ + template + void setPropertyStep(const std::string& name, T init, T step); + /** + * Array property element equivalent of setPropertyStep() + * Increment named environment property with a user defined step + * value = init + index * step + * @param name The name of the environment property to set + * @param index The index of the element within the environment property array to set + * @param init The value of the first environment property + * @param step The value to increment each subsequent environment property by + * @tparam T The type of the environment property, this must match the ModelDescription + * @throws exception::InvalidEnvProperty If a property of the name does not exist + * @throws exception::InvalidEnvPropertyType If a property with the name has a type different to T, or length > 1 + * @throws exception::OutOfBoundsException If this vector has a length less than 2 + * @see setPropertyStep(const std::string&, T, T) + * @see setPropertyLerpRange(const std::string&, flamegpu::size_type, T, T) + */ + template + void setPropertyStep(const std::string& name, flamegpu::size_type index, T init, T step); /** * Seed the internal random generator used for random property distributions * This will only affect subsequent calls to setPropertyRandom() @@ -471,6 +506,55 @@ void RunPlanVector::setPropertyLerpRange(const std::string &name, const flamegpu } } +template +void RunPlanVector::setPropertyStep(const std::string& name, T init, const T step) { + // Validation + const auto it = environment->find(name); + if (it == environment->end()) { + THROW exception::InvalidEnvProperty("Environment description does not contain property '%s', " + "in RunPlanVector::setPropertyStep()\n", + name.c_str()); + } + if (it->second.data.type != std::type_index(typeid(T))) { + THROW exception::InvalidEnvPropertyType("Environment property '%s' type mismatch '%s' != '%s', " + "in RunPlanVector::setPropertyStep()\n", + name.c_str(), it->second.data.type.name(), std::type_index(typeid(T)).name()); + } + if (it->second.data.elements != 1) { + THROW exception::InvalidEnvPropertyType("Environment property '%s' is an array with %u elements, array method should be used, " + "in RunPlanVector::setPropertyStep()\n", + name.c_str(), it->second.data.elements); + } + for (auto& i : *this) { + i.setProperty(name, init); + init += step; + } +} +template +void RunPlanVector::setPropertyStep(const std::string& name, const flamegpu::size_type index, T init, const T step) { + // Validation + const auto it = environment->find(name); + if (it == environment->end()) { + THROW exception::InvalidEnvProperty("Environment description does not contain property '%s', " + "in RunPlanVector::setPropertyStep()\n", + name.c_str()); + } + if (it->second.data.type != std::type_index(typeid(T))) { + THROW exception::InvalidEnvPropertyType("Environment property '%s' type mismatch '%s' != '%s', " + "in RunPlanVector::setPropertyStep()\n", + name.c_str(), it->second.data.type.name(), std::type_index(typeid(T)).name()); + } + const unsigned int t_index = detail::type_decode::len_t * index + detail::type_decode::len_t; + if (t_index > it->second.data.elements || t_index < index) { + throw exception::OutOfBoundsException("Environment property array index out of bounds " + "in RunPlanVector::setPropertyStep()\n"); + } + for (auto& i : *this) { + i.setProperty(name, index, init); + init += step; + } +} + template void RunPlanVector::setPropertyRandom(const std::string &name, rand_dist &distribution) { // Validation diff --git a/swig/python/flamegpu.i b/swig/python/flamegpu.i index 4ef0516e3..9ad11ec68 100644 --- a/swig/python/flamegpu.i +++ b/swig/python/flamegpu.i @@ -957,6 +957,7 @@ TEMPLATE_VARIABLE_INSTANTIATE_ID(setProperty, flamegpu::RunPlanVector::setProper TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_ID(setProperty, flamegpu::RunPlanVector::setProperty) TEMPLATE_VARIABLE_INSTANTIATE_ID(setPropertyArray, flamegpu::RunPlanVector::setPropertyArray) TEMPLATE_VARIABLE_INSTANTIATE(setPropertyLerpRange, flamegpu::RunPlanVector::setPropertyLerpRange) +TEMPLATE_VARIABLE_INSTANTIATE(setPropertyStep, flamegpu::RunPlanVector::setPropertyStep) TEMPLATE_VARIABLE_INSTANTIATE(setPropertyUniformRandom, flamegpu::RunPlanVector::setPropertyUniformRandom) TEMPLATE_VARIABLE_INSTANTIATE_FLOATS(setPropertyNormalRandom, flamegpu::RunPlanVector::setPropertyNormalRandom) TEMPLATE_VARIABLE_INSTANTIATE_FLOATS(setPropertyLogNormalRandom, flamegpu::RunPlanVector::setPropertyLogNormalRandom) diff --git a/tests/python/simulation/test_RunPlanVector.py b/tests/python/simulation/test_RunPlanVector.py index e1820805b..1b913801b 100644 --- a/tests/python/simulation/test_RunPlanVector.py +++ b/tests/python/simulation/test_RunPlanVector.py @@ -177,7 +177,6 @@ def test_setProperty(self): assert e.value.type() == "OutOfBoundsException" # Check that all values set lie within the min and max inclusive - # @todo - should fp be [min, max) like when using RNG? def test_setPropertyLerpRange(self): # Define the simple model to use model = pyflamegpu.ModelDescription("test") @@ -194,6 +193,81 @@ def test_setPropertyLerpRange(self): plans = pyflamegpu.RunPlanVector(model, totalPlans) # No need to seed the random, as this is a LERP rather than a random distribution. + # Uniformly set each property to a new value, then check it has been applied correctly. + fMin = 1. + fStep = 100. + iMin = 1 + iStep = 100 + u3Min = (1, 101, 201) + u3Step = (100, 200, 300) + # void setPropertyStep(const std::string &name, const T &init, const T &step) + plans.setPropertyStepFloat("f", fMin, fStep) + plans.setPropertyStepInt("i", iMin, iStep) + # Check setting individual array elements + # void setPropertyStep(const std::string &name, const EnvironmentManager::size_type &index, const T &init, const T &step) + plans.setPropertyStepUInt("u3", 0, u3Min[0], u3Step[0]) + plans.setPropertyStepUInt("u3", 1, u3Min[1], u3Step[1]) + plans.setPropertyStepUInt("u3", 2, u3Min[2], u3Step[2]) + # Check values are as expected by accessing the properties from each plan + i = 0 + for plan in plans: + assert plan.getPropertyFloat("f") >= fMin + (i-0.01) * fStep + assert plan.getPropertyFloat("f") <= fMin + (i+0.01) * fStep + assert plan.getPropertyInt("i") == iMin + i * iStep + u3FromPlan = plan.getPropertyArrayUInt("u3") + assert u3FromPlan[0] == u3Min[0] + i * u3Step[0] + assert u3FromPlan[1] == u3Min[1] + i * u3Step[1] + assert u3FromPlan[2] == u3Min[2] + i * u3Step[2] + i += 1 + + + # Tests for exceptions + # -------------------- + singlePlanVector = pyflamegpu.RunPlanVector(model, 1) + # Note litereals used must match the templated type not the incorrect types used, to appease MSVC warnings. + # void RunPlanVector::setPropertyStep(const std::string &name, const T &init, const T &step) + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + plans.setPropertyStepFloat("does_not_exist", 1., 100.) + assert e.value.type() == "InvalidEnvProperty" + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + plans.setPropertyStepFloat("i", 1., 100.) + assert e.value.type() == "InvalidEnvPropertyType" + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + plans.setPropertyStepUInt("u3", 1, 100) + assert e.value.type() == "InvalidEnvPropertyType" + # void RunPlanVector::setPropertyStep(const std::string &name, const EnvironmentManager::size_type, const T &init, const T &step) + # Extra brackets within the macro mean commas can be used due to how preproc tokenizers work + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + plans.setPropertyStepFloat("does_not_exist", 0, 1., 100.) + assert e.value.type() == "InvalidEnvProperty" + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + plans.setPropertyStepFloat("u3", 0, 1., 100.) + assert e.value.type() == "InvalidEnvPropertyType" + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + minus_one_uint32_t = -1 & 0xffffffff + plans.setPropertyStepUInt("u3", minus_one_uint32_t, 1, 100) + assert e.value.type() == "OutOfBoundsException" + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + plans.setPropertyStepUInt("u3", 4, 1, 100) + assert e.value.type() == "OutOfBoundsException" + + # Check that all values match expected + def test_setPropertyStep(self): + # Define the simple model to use + model = pyflamegpu.ModelDescription("test") + # Add a few environment properties to the model. + environment = model.Environment() + fOriginal = 0.0 + iOriginal = 0 + u3Original = (0, 0, 0) + environment.newPropertyFloat("f", fOriginal) + environment.newPropertyInt("i", iOriginal) + environment.newPropertyArrayUInt("u3", u3Original) + # Create a vector of plans + totalPlans = 10 + plans = pyflamegpu.RunPlanVector(model, totalPlans) + # No need to seed the random, as this is a LERP rather than a random distribution. + # Uniformly set each property to a new value, then check it has been applied correctly. fMin = 1. fMax = 100. @@ -259,7 +333,7 @@ def test_setPropertyLerpRange(self): with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: plans.setPropertyLerpRangeUInt("u3", 4, 1, 100) assert e.value.type() == "OutOfBoundsException" - + # Checking for uniformity of distribution would require a very large samples size. # As std:: is used, we trust the distribution is legit, and instead just check for min/max. def test_setPropertyUniformRandom(self): diff --git a/tests/test_cases/simulation/test_RunPlanVector.cu b/tests/test_cases/simulation/test_RunPlanVector.cu index c1e021a3b..ca899ec1c 100644 --- a/tests/test_cases/simulation/test_RunPlanVector.cu +++ b/tests/test_cases/simulation/test_RunPlanVector.cu @@ -181,7 +181,6 @@ double t_lerp(const T _min, const T _max, const double a) { } // Check that all values set lie within the min and max inclusive -// @todo - should fp be [min, max) like when using RNG? TEST(TestRunPlanVector, setPropertyUniformDistribution) { // Define the simple model to use flamegpu::ModelDescription model("test"); @@ -223,6 +222,7 @@ TEST(TestRunPlanVector, setPropertyUniformDistribution) { plans.setPropertyLerpRange("u3", 2, u3Min[2], u3Max[2]); plans.setPropertyLerpRange("f2", 0, f2Min[0], f2Max[0]); plans.setPropertyLerpRange("f2", 1, f2Min[1], f2Max[1]); + // Check values are as expected by accessing the properties from each plan int i = 0; const double divisor = totalPlans - 1; @@ -258,6 +258,81 @@ TEST(TestRunPlanVector, setPropertyUniformDistribution) { EXPECT_THROW((plans.setPropertyLerpRange("u3", static_cast(-1), 1u, 100u)), exception::OutOfBoundsException); EXPECT_THROW((plans.setPropertyLerpRange("u3", 4u, 1u, 100u)), exception::OutOfBoundsException); } +// Check that all values set lie within the min and max inclusive +TEST(TestRunPlanVector, setPropertyStep) { + // Define the simple model to use + flamegpu::ModelDescription model("test"); + // Add a few environment properties to the model. + auto environment = model.Environment(); + const float fOriginal = 0.0f; + const int32_t iOriginal = 0; + const std::array u3Original = { {0, 0, 0} }; + const std::array f2Original = { {12.0f, 13.0f} }; + environment.newProperty("f", fOriginal); + environment.newProperty("fb", fOriginal); + environment.newProperty("i", iOriginal); + environment.newProperty("u3", u3Original); + environment.newProperty("f2", f2Original); + // Create a vector of plans + constexpr uint32_t totalPlans = 10u; + flamegpu::RunPlanVector plans(model, totalPlans); + // No need to seed the random, as does not use a random distribution. + + // Uniformly set each property to a new value, then check it has been applied correctly. + const float fMin = 1.f; + const float fStep = 100.f; + const float fbMin = 0.0f; + const float fbStep = 1.0f; + const int32_t iMin = 1; + const int32_t iStep = 100; + const std::array u3Min = { {1u, 101u, 201u} }; + const std::array u3Step = { {100u, 200u, 300u} }; + const std::array f2Min = { {1.0f, 100.f} }; + const std::array f2Step = { {0.0f, -100.0f} }; + // void setPropertyStep(const std::string &name, T init, T step); + plans.setPropertyStep("f", fMin, fStep); + plans.setPropertyStep("fb", fbMin, fbStep); + plans.setPropertyStep("i", iMin, iStep); + // Check setting individual array elements + // void setPropertyStep(const std::string &name, flamegpu::size_type index, T init, T step); + plans.setPropertyStep("u3", 0, u3Min[0], u3Step[0]); + plans.setPropertyStep("u3", 1, u3Min[1], u3Step[1]); + plans.setPropertyStep("u3", 2, u3Min[2], u3Step[2]); + plans.setPropertyStep("f2", 0, f2Min[0], f2Step[0]); + plans.setPropertyStep("f2", 1, f2Min[1], f2Step[1]); + + // Check values are as expected by accessing the properties from each plan + int i = 0; + for (const auto& plan : plans) { + EXPECT_EQ(plan.getProperty("f"), fMin + i * fStep); + EXPECT_EQ(plan.getProperty("fb"), fbMin + i * fbStep); + const std::array f2FromPlan = plan.getProperty("f2"); + EXPECT_EQ(f2FromPlan[0], f2Min[0] + i * f2Step[0]); + EXPECT_EQ(f2FromPlan[1], f2Min[1] + i * f2Step[1]); + // Note integer values are rounded + EXPECT_EQ(plan.getProperty("i"), iMin + i * iStep); + const std::array u3FromPlan = plan.getProperty("u3"); + EXPECT_EQ(u3FromPlan[0], u3Min[0] + i * u3Step[0]); + EXPECT_EQ(u3FromPlan[1], u3Min[1] + i * u3Step[1]); + EXPECT_EQ(u3FromPlan[2], u3Min[2] + i * u3Step[2]); + ++i; + } + + // Tests for exceptions + // -------------------- + flamegpu::RunPlanVector singlePlanVector(model, 1); + // Note literals used must match the templated type not the incorrect types used, to appease MSVC warnings. + // void RunPlanVector::setPropertyStep(const std::string &name, T init, T step) + EXPECT_THROW((plans.setPropertyStep("does_not_exist", 1.f, 100.f)), flamegpu::exception::InvalidEnvProperty); + EXPECT_THROW((plans.setPropertyStep("i", 1.f, 100.f)), flamegpu::exception::InvalidEnvPropertyType); + EXPECT_THROW((plans.setPropertyStep("u3", 1u, 100u)), flamegpu::exception::InvalidEnvPropertyType); + // void RunPlanVector::setPropertyStep(const std::string &name, const flamegpu::size_type, T init, T step) + // Extra brackets within the macro mean commas can be used due to how preproc tokenizers work + EXPECT_THROW((plans.setPropertyStep("does_not_exist", 0u, 1.f, 100.f)), flamegpu::exception::InvalidEnvProperty); + EXPECT_THROW((plans.setPropertyStep("u3", 0u, 1.f, 100.f)), flamegpu::exception::InvalidEnvPropertyType); + EXPECT_THROW((plans.setPropertyStep("u3", static_cast(-1), 1u, 100u)), exception::OutOfBoundsException); + EXPECT_THROW((plans.setPropertyStep("u3", 4u, 1u, 100u)), exception::OutOfBoundsException); +} // Checking for uniformity of distribution would require a very large samples size. // As std:: is used, we trust the distribution is legit, and instead just check for min/max. TEST(TestRunPlanVector, setPropertyUniformRandom) {