Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4551 - Add minimum/maximum values to numeric OSArguments and use it in validateUserArgument #4621

Merged
merged 6 commits into from Aug 12, 2022

Conversation

jmarrec
Copy link
Collaborator

@jmarrec jmarrec commented Jun 29, 2022

Pull request overview

Pull Request Author

  • Model API Changes / Additions
  • Any new or modified fields have been implemented in the EnergyPlus ForwardTranslator (and ReverseTranslator as appropriate)
  • Model API methods are tested (in src/model/test)
  • EnergyPlus ForwardTranslator Tests (in src/energyplus/Test)
  • If a new object or method, added a test in NREL/OpenStudio-resources: Add Link
  • If needed, added VersionTranslation rules for the objects (src/osversion/VersionTranslator.cpp)
  • Verified that C# bindings built fine on Windows, partial classes used as needed, etc.
  • All new and existing tests passes
  • If methods have been deprecated, update rest of code to use the new methods

Labels:

  • If change to an IDD file, add the label IDDChange
  • If breaking existing API, add the label APIChange
  • If deemed ready, add label Pull Request - Ready for CI so that CI builds your PR

Review Checklist

This will not be exhaustively relevant to every PR.

  • Perform a Code Review on GitHub
  • Code Style, strip trailing whitespace, etc.
  • All related changes have been implemented: model changes, model tests, FT changes, FT tests, VersionTranslation, OS App
  • Labeling is ok
  • If defect, verify by running develop branch and reproducing defect, then running PR and reproducing fix
  • If feature, test running new feature, try creative ways to break it
  • CI status: all green or justified

@jmarrec jmarrec added component - Measures component - Measure Manager Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. labels Jun 29, 2022
@jmarrec jmarrec self-assigned this Jun 29, 2022
@@ -86,7 +86,7 @@ def initialize(logger=nil)
@measures = {} # measure_dir => BCLMeasure
@measure_info = {} # measure_dir => {osm_path => RubyUserScriptInfo}

eval(OpenStudio::Ruleset::infoExtractorRubyFunction)
eval(OpenStudio::Measure::infoExtractorRubyFunction)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modernize our script to not use the deprecated namespace

Comment on lines +473 to +483
if argument.hasDomain
min, max = argument.domainAsDouble
# I'm a bit wary of rounding issues... I think 1e308 instead of
# Float::MAX (1.7976931348623157e+308) is fine for our applications...
if min > -1e308 # -Float::MAX
arg[:min_value] = min
end
if max < 1e308 # Float::MAX
arg[:max_value] = max
end
end
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openstudio measure --compute-arguments will now output the min/max.

@@ -763,7 +763,7 @@ namespace measure {
return false;
}

double minValue = std::numeric_limits<double>::min();
double minValue = std::numeric_limits<double>::lowest();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using min() here means using the smallest positive value. NOT what was expected.

Use lowest() which equals -max()

cf https://godbolt.org/z/K1aq87Yjh

std::numeric_limits<double>::max()   =  1.7976931348623157e+308
std::numeric_limits<double>::min()   =  2.2250738585072014e-308  # POSITIVE minimum finite value
std::numeric_limits<double>::lowest()= -1.7976931348623157e+308

Comment on lines +164 to +184
if (argument.type() == OSArgumentType::Integer) {
auto domain = argument.domainAsInteger();
auto& low = domain.front();
auto& high = domain.back();
if (low > std::numeric_limits<int>::lowest()) {
minValue = std::to_string(low);
}
if (high < std::numeric_limits<int>::max()) {
maxValue = std::to_string(high);
}
} else if (argument.type() == OSArgumentType::Double) {
auto domain = argument.domainAsDouble();
auto& low = domain.front();
auto& high = domain.back();
if (low > std::numeric_limits<double>::lowest()) {
minValue = std::to_string(low);
}
if (high < std::numeric_limits<double>::max()) {
maxValue = std::to_string(high);
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this TODO so I also addressed it. It will get passes to BCLMeasureArgument => the measure.xml now will also include the min/max values

image

Comment on lines +530 to +545
} else if (typeValue == OSArgumentType::Double) {
auto value = user_argument.hasValue() ? user_argument.valueAsDouble() : user_argument.defaultValueAsDouble();
if (script_argument.hasDomain()) {
// Validate it's in the domain
auto domain = script_argument.domainAsDouble();
auto& low = domain.front();
auto& high = domain.back();
if (value < low || value > high) {
registerError(fmt::format("{} User argument '{}' has a value '{}' that is not in the domain [{}, {}].",
user_argument.type().valueName(), script_argument.name(), value, low, high));
result = false;
}
break;
case OSArgumentType::String:
case OSArgumentType::Choice:
case OSArgumentType::Path:
if (user_argument.hasValue()) {
stepValues.push_back(WorkflowStepValue(user_argument.name(), user_argument.valueAsString()));
} else if (user_argument.hasDefaultValue()) {
stepValues.push_back(WorkflowStepValue(user_argument.name(), user_argument.defaultValueAsString()));
}
if (result) {
stepValues.emplace_back(user_argument.name(), value);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the actual validation of the Double/Integer domain inside valdiateUserArguments => registers an actual error if out of bounds

Comment on lines +406 to +412
OSArgument arg = OSArgument::makeDoubleArgument("double_arg", true);
arg.setMaxValue(10.0);
result.push_back(arg);

arg = OSArgument::makeIntegerArgument("int_arg", true);
arg.setMinValue(0);
result.push_back(arg);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test

Comment on lines +449 to +482
// call with a good value
double_arg.setValue(-1.0);
int_arg.setValue(1.0);
EXPECT_TRUE(script.run(model, runner, user_arguments));
WorkflowStepResult result = runner.result();
ASSERT_TRUE(result.stepResult());
EXPECT_EQ(StepResult::Success, result.stepResult()->value());
EXPECT_EQ(0u, result.stepErrors().size());
EXPECT_EQ(0u, result.stepWarnings().size());

// Out of bound value for double_arg
runner.reset();
double_arg.setValue(100.0);
EXPECT_FALSE(script.run(model, runner, user_arguments));
result = runner.result();
ASSERT_TRUE(result.stepResult());
EXPECT_EQ(StepResult::Fail, result.stepResult()->value());
ASSERT_EQ(1u, result.stepErrors().size());
EXPECT_EQ(
fmt::format("Double User argument 'double_arg' has a value '100' that is not in the domain [{}, 10].", std::numeric_limits<double>::lowest()),
result.stepErrors()[0]);
EXPECT_EQ(0u, result.stepWarnings().size());

// Out of bound value for int_arg
runner.reset();
double_arg.setValue(1.0);
int_arg.setValue(-3);
EXPECT_FALSE(script.run(model, runner, user_arguments));
result = runner.result();
ASSERT_TRUE(result.stepResult());
EXPECT_EQ(StepResult::Fail, result.stepResult()->value());
ASSERT_EQ(1u, result.stepErrors().size());
EXPECT_EQ("Integer User argument 'int_arg' has a value '-3' that is not in the domain [0, 2147483647].", result.stepErrors()[0]);
EXPECT_EQ(0u, result.stepWarnings().size());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As can be seen, it does capture the issue of the out of bounds.

If you run the test with e2d00da (before I changed the std::numeric_limits<T>::min() to std::numeric_limits<T>::lowest()) it fails (I did that fix before extending the tests but reordered the commits to show the issue)

@jmarrec
Copy link
Collaborator Author

jmarrec commented Jun 30, 2022

@DavidGoldwasser Could you take a look at this one please? And confirm adding the min_value / max_value to the measure.xml is ok with you?


One thing I also wanted to do was to replace JSON.generate by JSON.pretty_generate for --compute-arguments and --update (not update_all), so the resulting JSON is actually readable by a human instead of being a condensed one-liner.

safe_puts JSON.generate(hash)

safe_puts JSON.generate(hash)

Thoughts anyone?

@jmarrec jmarrec added this to the OpenStudio SDK 3.5.0 milestone Jul 26, 2022
@@ -468,24 +470,42 @@ def get_arguments_from_measure_info(measure_info)
elsif type == "Double".to_OSArgumentType
arg[:units] = argument.units.get if argument.units.is_initialized
arg[:default_value] = argument.defaultValueAsDouble if argument.hasDefaultValue

if argument.hasDomain
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it still have domain if either min or max are set but not both?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain is set as [std::numeric_limits<double>::lowest(), max] if max only is provided, and if min only then [min, std::numeric_limits<double>::max()]


if argument.hasDomain
min, max = argument.domainAsInteger
if min != -2147483648
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused what the purpose of this is? Same for above with 1e308.

Copy link
Collaborator Author

@jmarrec jmarrec Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as above: when only min or max is provided, one of the domain boundaries is going to be std::numeric_limits<T>::lowest() or std::numeric_limits<T>::max().

  • In the case of an integer that's +- 2147483648
  • for a double that's +-1.7976931348623157e+308

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if you did indeed set a min less than lowest() or a max greater than max(), wouldn't it not set/write those values?

Copy link
Collaborator Author

@jmarrec jmarrec Jul 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how you could do that though. It'd be an overflow error, but mostly you just can't do it, bindings included.

cf:

$ irb
2.7.2 :002 > 1e308
 => 1.0e+308 
2.7.2 :003 > 1e309
 => Infinity 

result.push_back(arg);

arg = OSArgument::makeIntegerArgument("int_arg", true);
arg.setMinValue(0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test with a negative?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component - Measure Manager component - Measures Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add minimum/maximum values to numeric OSArguments
3 participants