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

Integrate sin, cos and constants to Storm Jani parser #556

Merged

Conversation

MarcoLm993
Copy link
Contributor

This PR introduces support to the sin and cos operators, and to the PI and Euler number constants.

In particular, for the sin and cos operators I introduced a new expression type called trigonometric, that can be used later on in case other trigonometric operators will be integrated in storm.

Please let me know if I missed any additional place where the aforementioned operators need to be defined.

@MarcoLm993 MarcoLm993 force-pushed the support-for-sin-cos-constants branch from be3eeb3 to 77ad64e Compare May 27, 2024 11:58
@@ -1432,6 +1442,16 @@ storm::expressions::Expression JaniParser<ValueType>::parseExpression(Json const
STORM_LOG_THROW(false, storm::exceptions::InvalidJaniException, "Unknown operator " << opstring << " in " << scope.description << ".");
}
}
if (expressionStructure.count("constant") == 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Here, the expression now represents an approximation of the actual expression in the jani file. This is something to discuss a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I understand it correctly, you suggest to have something like this for PI (and the same for e):

if (constantStr == "π") {
  return storm::expressions::pi();
}

Did I get it right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hello,
I started looking into implementing this and I am not sure on what would be the preferred way to go.
My current understanding is that we need to have an additional class inherited from BaseExpression, that we can call ConstantValueExpression (or anything else), and this would provide an enum for the ConstantValue to store (i.e. PI and E), together with the same methods as for the RationalLiteralExpression class.

Taking this path means that some more boilerplate code needs to be written (for now I only found all the classes inherited from ExpressionVisitor, but maybe there is more), so I want to ask whether this is what you had in mind with your comment above, before I start implementing everything that is required.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tquatmann your input would be greatly appreciated here

Copy link
Contributor

Choose a reason for hiding this comment

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

I personally would indeed tend to use a ConstantValueExpression or maybe a RealLiteralExpression?

Copy link
Member

Choose a reason for hiding this comment

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

What you describe is the most correct and most complete way to implement this. However, unless someone sees an important use case for π and e outside of jani models, I suggest to reduce the mentioned boilerplate code a bit:

  • Put your new expression class here, where we collect all expressions that are somewhat special for JANI. Given the current naming scheme, the new class should have suffix ...LiteralExpression, maybe TranscendentalNumberLiteralExpression ?

  • Extend the JaniExpressionVisitor and every class that inherits from this (fortunately, these are only a hand full and it's almost always clear what to do).

  • Extend the substituteConstantsInPlace function of the JANI model, so that it can (optionally, controlled by an argument) approximate transcendentalNumberLiterals. Apply this substitution, before building the model.

The advantage of this approach is that you can still export the parsed jani without approximating the constants.
For example, when using storm-conv --jani input.jani --constants N=4 --tojani output.jani, the constants should still be present.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, having the new ...LiteralExpression class in jani/expressions seems more appropriate for the current PR, thanks for your suggestion!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @tquatmann ,

I just pushed a first attempt at integrating the TranscendentalNumberLiteralExpression in the JaniParser.
I am not sure this is 100% what you had in mind, since it resulted in an unexpected amount of changes around the code. Please let me know if there is anything that you think should be done differently.

Also, the unit tests and all the remaining comments will come at a later commit. This is just to make sure the changes related to this comment match your requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just pushed some more changes I needed to do in order to have storm-conv working as you described.
Somehow this ended up touching an unexpected amount of files, I hope this is still fine with you, but of course I am open to any suggestion you might have.

In the next days I will address the last few comments you had (namely a missing type check and accepting integers as argument of trigonometric operators).

@sjunges
Copy link
Contributor

sjunges commented May 27, 2024

Hi Marco,

great!

  • I think that it is debatable whether pi should be the same as 3.14.... I currently would argue that as an expression, it should be a function wihtout arguments (a symbol, that is) and only upon evaluating it, it should be interpreted with an approximation. @tquatmann @volkm

  • It would be good if you could add some small tests, i.e., a small programme with sin/cos and pi/e and show that this gets properly parsed.

  • We probably do not handle model building for jani models with these constructs. Thus, I guess that the Jani Builders should get methods that check whether these ingredients are in the jani model and refuse working with them. One approach here would be to use the model features of Jani. If trig is not added as a model feature, the parser should complain (or at least during a 'finalize' call) and the model builders should check whether this model feature is present. Let us know if you have any questions about this!

Best,
Sebastian

@MarcoLm993
Copy link
Contributor Author

Hi Sebastian,

thanks for the very quick feedback!

  • Point 1, I replied in the initial comment, I think i understand your point.
  • Point 2, I already have a small jani model to check that the new features are parsed correctly, I can add that in STORM, too, for the unit test!
  • Point 3, I think I understood your point, I will try to have a look and reach out in case of doubts!

Best,
Marco

@sjunges sjunges self-assigned this May 29, 2024
case OperatorType::Cos:
case OperatorType::Sin:
// The operands expect a real value. Raise an exception
STORM_LOG_THROW(false, storm::exceptions::InvalidOperationException, "Cannot apply cos or sin to an integer value.");
Copy link
Member

Choose a reason for hiding this comment

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

My understanding of the JANI-spec is that e.g. sin(10) would be valid in JANI as int is also a valid numeric type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is true. At the same time, the operator's output will always be a rational value, regardless of the input type. Anyway, I will add support for having integers as input.

@MarcoLm993 MarcoLm993 force-pushed the support-for-sin-cos-constants branch from 77ad64e to 6fb451b Compare June 10, 2024 13:07
@MarcoLm993
Copy link
Contributor Author

Hello @sjunges and @tquatmann ,

I just pushed the last changes related to this PR. Now it should be good to go, if you do not spot anything else to change. :)

Looking forward to your inputs.

Best,
Marco

return *this;
}

Model Model::substituteConstants() const {
Model result(*this);
result.replaceUnassignedVariablesWithConstants();
result.substituteConstantsInPlace();
result.substituteConstantsInPlace(false);
return result;
}

Model Model::substituteConstantsFunctions() const {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added to this method the substitution of Transcendental numbers with Rational numbers, too.
Do you think it makes sense to reflect that in the name of the method, too? Something like substituteConstantsFunctionsTranscendentals?

Copy link
Member

Choose a reason for hiding this comment

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

Right now, it is a bit inconsistent that substituteConstants does not substitute transcendentals, while substituteConstantsFunctions does. Changing the name as you suggest sounds good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perfect, will do it!

@@ -156,6 +158,7 @@ bool JaniNextStateGenerator<ValueType, StateType>::canHandle(storm::jani::Model
features.remove(storm::jani::ModelFeature::DerivedOperators);
features.remove(storm::jani::ModelFeature::Functions); // can be substituted
features.remove(storm::jani::ModelFeature::StateExitRewards);
features.remove(storm::jani::ModelFeature::TrigonometricFunctions);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this true? If so, I think this should be covered in a test.
If you are not sure, I would not allow this right now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hello Sebastian,

I am using it in the smc extension we are developing in the CONVINCE, so I am quite convinced this is working.
However, having one more test never hurts! :)

Some references about the smc use case:

Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to make sure that we dont break this, a test definitively does not hurt :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I adapted the test_trigonometry jani file to not expand forever and added a test for it in the ExplicitJaniModelBuilderTest gtest, (that is the only place I found with similar tests).
The new unit test can be found in this commit: 6b54f7c
On doing it, I found that transcendental numbers were not handled correctly in the constant variables bounds, but now they are handled, too.

@@ -187,7 +196,7 @@ class IntegerType : public BaseType {
virtual bool isIntegerType() const override;

private:
static const uint64_t mask = (1ull << 62);
static const uint64_t mask = (1ull << 58);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason these have changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it was required for the type comparison here implemented here: to ensure that a transcendental number type is considered a non-integer number by the various model visitors, its mask must evaluate to a value higher than the one inn IntegerType.
Since in this implementation the mask is generated by setting a single bit in a uint64 variable, RationalType was getting the highest possible value, and IntegerType the one right before that. For this reason, I had to shift all of them to make space for the newly implemented TranscendentalNumberType.

Copy link
Member

Choose a reason for hiding this comment

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

Why is the shift decreased by 4 and not just by 1? My understanding is that masks (1ull << 61), (1ull << 62) and (1ull << 63) are now unused, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When introducing the TranscendentalNumbers, I had to do the shift because I needed the related type mask value to be greater than the rational numbers one, that was using the highest possible value.
While implementing the shift, I thought that it would have been much easier to add the new mask if the last value wasn't already used up, and following that reasoning, I made sure that there were some unused values before and after the existing Type masks.

Functionality wise, this makes no difference, but it makes it easier to add new types in the future, if that will be ever needed.

Anyway, if you prefer to have it as it was before, I can make sure there are no unused values starting from the last one.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for clarifying. I just wanted to make sure that I didn't miss some detail concerning the unused masks.

void TemplateEdgeDestination::substitute(std::map<storm::expressions::Variable, storm::expressions::Expression> const& substitution) {
assignments.substitute(substitution);
void TemplateEdgeDestination::substitute(std::map<storm::expressions::Variable, storm::expressions::Expression> const& substitution,
bool const& substituteTranscendentalNumbers) {
Copy link
Contributor

Choose a reason for hiding this comment

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

bool by const& triggers a warning on my system

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can remove the reference. On my system quite some warning are triggered, so it is hard to notice new ones.
Are there more warnings that were added by this PR?

Copy link
Contributor Author

@MarcoLm993 MarcoLm993 Jun 19, 2024

Choose a reason for hiding this comment

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

I had a look in the repository and bool const& seems to be a common pattern for function definitions in STORM.

Of course I can switch the ones I added to be bool const, by dropping the reference, but maybe it would make sense to make that consistent throughout the whole repository. The same could be done for all other basic types (int, float, double, char).

In case, I believe that should be tackled in a separated PR :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed all declarations of bool const& substituteTranscendentalNumbers to not pass the value by reference anymore here: 6f016ca
On my machine, this seems not to throw warnings anymore.


for (auto& assignment : assignments) {
assignment.substitute(substitution);
assignment.substitute(substitution, substituteTranscendentalNumbers);
Copy link
Contributor

Choose a reason for hiding this comment

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

is this maybe something that can be avoided by having a sensible default for this?

Copy link
Contributor Author

@MarcoLm993 MarcoLm993 Jun 19, 2024

Choose a reason for hiding this comment

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

I am not sure, since this is set in the JaniModel's methods to substitute constants (around here) and must be propagated down to the various entries of the model. The value depends whether we want to preserve the transcendental numbers (e.g. storm-conv) or not (when we run the evaluation).
A way to not have the this argument would be to set a static variable for the jani visitor, to signal whether we want to substitute transcendental numbers or not, but I wouldn't recommend it, since it makes the code execution less clear.

Copy link
Member

@tquatmann tquatmann left a comment

Choose a reason for hiding this comment

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

Thanks for doing all this! 👍

There are some rather minor comments left. Apart from that, this is looking good!

I believe we could eventually replace all these substitute methods by a JaniTraverser but that is something for a different PR.

@@ -187,7 +196,7 @@ class IntegerType : public BaseType {
virtual bool isIntegerType() const override;

private:
static const uint64_t mask = (1ull << 62);
static const uint64_t mask = (1ull << 58);
Copy link
Member

Choose a reason for hiding this comment

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

Why is the shift decreased by 4 and not just by 1? My understanding is that masks (1ull << 61), (1ull << 62) and (1ull << 63) are now unused, correct?

return *this;
}

Model Model::substituteConstants() const {
Model result(*this);
result.replaceUnassignedVariablesWithConstants();
result.substituteConstantsInPlace();
result.substituteConstantsInPlace(false);
return result;
}

Model Model::substituteConstantsFunctions() const {
Copy link
Member

Choose a reason for hiding this comment

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

Right now, it is a bit inconsistent that substituteConstants does not substitute transcendentals, while substituteConstantsFunctions does. Changing the name as you suggest sounds good to me.

@@ -60,7 +60,7 @@ std::shared_ptr<BaseExpression const> ConstructorArrayExpression::size() const {
std::shared_ptr<BaseExpression const> ConstructorArrayExpression::at(uint64_t i) const {
std::map<storm::expressions::Variable, storm::expressions::Expression> substitution;
substitution.emplace(indexVar, this->getManager().integer(i));
return storm::jani::substituteJaniExpression(elementExpression->toExpression(), substitution).getBaseExpressionPointer();
return storm::jani::substituteJaniExpression(elementExpression->toExpression(), substitution, true).getBaseExpressionPointer();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return storm::jani::substituteJaniExpression(elementExpression->toExpression(), substitution, true).getBaseExpressionPointer();
return storm::jani::substituteJaniExpression(elementExpression->toExpression(), substitution, false).getBaseExpressionPointer();

If I see this correctly, one wouldn't want to substitute transcendentalNumbers at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you are right, I am switching it to false in the upcoming commit!

@@ -66,10 +66,10 @@ storm::expressions::Expression const& BoundedType::getUpperBound() const {
void BoundedType::substitute(std::map<storm::expressions::Variable, storm::expressions::Expression> const& substitution) {
Copy link
Member

Choose a reason for hiding this comment

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

Why does this not pass a bool substituteTranscendentalNumbers ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I remember correctly, I was just afraid of growing the amount of changes too large, but looking at that again, there is no reason to not have the substituteTranscendentalNumbers flag here as well. I will add that to the upcoming commit!

…bstitution method in JaniModel

Signed-off-by: Marco Lampacrescia <[email protected]>
Copy link
Contributor Author

@MarcoLm993 MarcoLm993 left a comment

Choose a reason for hiding this comment

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

Hi, thanks for your review.
I addressed the comments from @tquatmann and pushed them in the last commit, and I added one comment about the bit shift for the Type maks.
Let me know what you think about it.

@@ -187,7 +196,7 @@ class IntegerType : public BaseType {
virtual bool isIntegerType() const override;

private:
static const uint64_t mask = (1ull << 62);
static const uint64_t mask = (1ull << 58);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When introducing the TranscendentalNumbers, I had to do the shift because I needed the related type mask value to be greater than the rational numbers one, that was using the highest possible value.
While implementing the shift, I thought that it would have been much easier to add the new mask if the last value wasn't already used up, and following that reasoning, I made sure that there were some unused values before and after the existing Type masks.

Functionality wise, this makes no difference, but it makes it easier to add new types in the future, if that will be ever needed.

Anyway, if you prefer to have it as it was before, I can make sure there are no unused values starting from the last one.

@@ -60,7 +60,7 @@ std::shared_ptr<BaseExpression const> ConstructorArrayExpression::size() const {
std::shared_ptr<BaseExpression const> ConstructorArrayExpression::at(uint64_t i) const {
std::map<storm::expressions::Variable, storm::expressions::Expression> substitution;
substitution.emplace(indexVar, this->getManager().integer(i));
return storm::jani::substituteJaniExpression(elementExpression->toExpression(), substitution).getBaseExpressionPointer();
return storm::jani::substituteJaniExpression(elementExpression->toExpression(), substitution, true).getBaseExpressionPointer();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you are right, I am switching it to false in the upcoming commit!

return *this;
}

Model Model::substituteConstants() const {
Model result(*this);
result.replaceUnassignedVariablesWithConstants();
result.substituteConstantsInPlace();
result.substituteConstantsInPlace(false);
return result;
}

Model Model::substituteConstantsFunctions() const {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perfect, will do it!

@@ -66,10 +66,10 @@ storm::expressions::Expression const& BoundedType::getUpperBound() const {
void BoundedType::substitute(std::map<storm::expressions::Variable, storm::expressions::Expression> const& substitution) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I remember correctly, I was just afraid of growing the amount of changes too large, but looking at that again, there is no reason to not have the substituteTranscendentalNumbers flag here as well. I will add that to the upcoming commit!

Copy link
Member

@tquatmann tquatmann left a comment

Choose a reason for hiding this comment

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

Thanks for looking into my comments. To me, this PR looks ready to be merged!

@sjunges
Copy link
Contributor

sjunges commented Jul 13, 2024

Many thanks for all the work.

@sjunges sjunges merged commit 7553a4a into moves-rwth:master Jul 13, 2024
15 checks passed
@MarcoLm993
Copy link
Contributor Author

I am the one thanking you for the discussions and for looking into this :)

volkm added a commit to volkm/stormpy that referenced this pull request Jul 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants