Skip to content

Add variability change for these functions. #3610

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

HansOlsson
Copy link
Collaborator

Based on discussion in modelica/ModelicaStandardLibrary#4495

I created it as a PR in order to have something concrete to discuss.
It is a draft PR since there are a number of issues to resolve (in addition to the obvious issues of an annotation having such an impact):

  • This corresponds to the intent of GenerateEvents, right?
  • No need for a separate annotation/construct for this, right?
  • What to do if the output is a record with both Real and non-Real members?
  • What to do if there are Real and non-Real outputs as separate outputs?

The reason it is a separate item is to make it easier to discuss.
I have slightly changed my mind on these items and will update the PR

So, an Integer output will be discrete-time, but a Real output still non-discrete time (assuming the inputs are non-discrete time).
@HansOlsson HansOlsson added the discussion Indicates that there's a discussion; not clear if bug, enhancement, or working as intended label Nov 12, 2024
Copy link
Collaborator

@henrikt-ma henrikt-ma left a comment

Choose a reason for hiding this comment

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

I think this would only have a chance of working if GenerateEvents = true was applied recursively also to all function calls in the function body. We need to avoid loopholes like this:

model DiscreteValuedFunctions
  function f
    input Real u;
    output Integer y = integer(u + 0.5); /* Non-discrete-time Integer output. */
    annotation(Inline = false); // Avoid SystemModeler issue.
  end f;

  function g
    input Real u;
    output Integer y = if u < 2 then 0 else f(u);
    annotation(GenerateEvents = true);
  end g;

  Real a = g(time);    // Fine, result with discontinuity at time 2.0
//Integer b = g(time); // Bad: b is assigned a non-discrete-time solution
  annotation(experiment(StopTime = 5.0, Interval = 0.1));
end DiscreteValuedFunctions;

Edit: Improved example.

@HansOlsson
Copy link
Collaborator Author

I think this would only have a chance of working if GenerateEvents = true was applied recursively also to all function calls in the function body.

A good point, and it's possible that such a recursive solution would work, but I would rather propose to rethink this in another way that gives a similar result in this case.

A function with GenerateEvents=true will sort of behave as a block and not a function in terms of events.
The draft proposal realizes that we need to extend this to having non-Real outputs behave as in a block, i.e. be discrete variables.
The new point is that inside such function we also need to have similar restrictions as in a block, so that Integer y = if u < 2 then 0 else f(u) triggers a variability error; as if it had been used in a block.
The solution for that error is then to apply the annotation in called functions, but nothing is directly applying the annotation recursively.

The problem with applying it recursively is that it must be done with care.
In MSL master we have e.g.,: Modelica.Media.R134a.R134a_ph.getPhase_ph

function getPhase_ph "Number of phases by pressure and specific enthalpy"
  extends Modelica.Icons.Function;
  input AbsolutePressure p "Pressure";
  input SpecificEnthalpy h "Specific enthalpy";

  output Integer phase "Number of phases";

protected 
  SaturationProperties sat(psat=p, Tsat=0)
    "Saturation temperature and pressure";
  SI.SpecificEnthalpy hl=bubbleEnthalpy(sat)
    "Liquid enthalpy";
  SI.SpecificEnthalpy hv=dewEnthalpy(sat) "Vapor enthalpy";

algorithm 
  phase := if ((h < hl) or (h > hv) or (p > R134aData.data.FPCRIT)) then 1
     else 2;

  annotation (GenerateEvents=true, Inline=true, Documentation(info="<html>
This function computes the number of phases for R134a depending on the inputs for absolute pressure and specific enthalpy. It makes use of cubic spline functions for liquid and vapor specific enthalpy.
</html>"));
end getPhase_ph;

Naively generating events would imply that bubbleEnthalpy should generate events, which would be a complete mess (and forcing users to write noEvent around it would be odd). However, if we just use the "as a block" rules there is no problem with that function call - so we can leave it as is.

We need to avoid loopholes like this:
...

Totally agree.

@HansOlsson
Copy link
Collaborator Author

I have now added some text for that. We could add an example showing how this requires that some called functions also use the annotation.

@henrikt-ma
Copy link
Collaborator

henrikt-ma commented Nov 29, 2024

I think we'll have to rethink the variabilities of function inputs when GenerateEvents = true. Currently, when passing a non-discrete-time Real expression to a function, this expression is considered having evaluable variability inside of the function body.

It would be great if setting GenerateEvents = true on a function would not make it less useful for computing discrete-valued results. Consider this valid model:

model M1
  function f
    input Real u;
    output Boolean y = u > 0.5;
  end f;
  Boolean b = f(1.0);
end M1;

It would seem unfortunate if the following would be a variability error inside the function:

model M2
  function f
    input Real u;
    output Boolean y = u > 0.5; /* Needs discrete-time variability for u. */
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2;

So the question is whether it would be possible to utilize the expression variability of arguments passed to the function, instead of only using the worst-case variability for the type of the input.

@henrikt-ma
Copy link
Collaborator

henrikt-ma commented Nov 29, 2024

I actually believe there is a natural and elegant solution to the problem with M2 above:

model M2Fix
  function f
    input discrete Real u;
    output Boolean y = u > 0.5;
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2Fix;

That is, declared variability of function inputs. It would primarily be useful for functions with GenerateEvents = true, but could also be considered useful for other functions, as a way of making the function ready for GenerateEvents = true in the future.

Some details:

  • Inside functions discrete would simply mean restricting variability to discrete-time.
  • When calling a function having an input declared constant, the passed expression only needs to have evaluable variability, but has to be evaluated. (That is, we use constant as a way to of saying `/* evaluated */ parameter.)
  • Declared variability of function outputs seems somewhat less useful, and potentially confusing in combination with GenerateEvents = false (since variability must be determined from the argument expression variabilities in this case). It would, however, still be useful in the case of output discrete Real y when GenerateEvents = true.

@HansOlsson
Copy link
Collaborator Author

I think we'll have to rethink the variabilities of function inputs when GenerateEvents = true. Currently, when passing a non-discrete-time Real expression to a function, this expression is considered having evaluable variability inside of the function body.

It would be great if setting GenerateEvents = true on a function would not make it less useful for computing discrete-valued results. Consider this valid model:

model M1
  function f
    input Real u;
    output Boolean y = u > 0.5;
  end f;
  Boolean b = f(1.0);
end M1;

It would seem unfortunate if the following would be a variability error inside the function:

model M2
  function f
    input Real u;
    output Boolean y = u > 0.5; /* Needs discrete-time variability for u. */
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2;

So the question is whether it would be possible to utilize the expression variability of arguments passed to the function, instead of only using the worst-case variability for the type of the input.

I see the point, and agree to an extent - and will return to that.

However, it doesn't need discrete-time variability for u if we use the worst-case variability for the input, as u > 0.5 normally generates an event, a non-discrete-time Real.

I can agree that it is inefficient to generate an event for a literal (or parameter) input, but I don't see that it is an error.

It is also a bit confusing that the outputs from functions have two variabilities:

  • Inside the function the Boolean output behave as a discrete-time expression.
  • Outside the function the function-call-result (which corresponds to the output) is a constant expression.

But that isn't a new problem, and I don't see that it generates problems.

In practice the usual implementation of GenerateEvents=true is to simply inline the function (in contrast to the complicated inlining for other functions that need to handle events), and after that the expression variability of the arguments will be used to avoid generating events.
And that's how I see the functions using the variability of the expression argument.

@henrikt-ma
Copy link
Collaborator

henrikt-ma commented Nov 29, 2024

However, it doesn't need discrete-time variability for u if we use the worst-case variability for the input, as u > 0.5 normally generates an event, a non-discrete-time Real.

Sorry, my example was bad. What I meant was rather something like this:

model M2b
  function g
    input Real u;
    output Boolean y = u > 0.5;
  end g;
  function f
    input Real u;
    output Boolean y = g(u); /* Needs discrete-time variability for u. */
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2b;

Again, the elegant solution to this would be the following:

model M2bFix
  function g
    input Real u;
    output Boolean y = u > 0.5;
  end g;
  function f
    input discrete Real u;
    output Boolean y = g(u); /* Fine; u, and hence g(u), have discrete-time variability. */
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2bFix;

@HansOlsson
Copy link
Collaborator Author

However, it doesn't need discrete-time variability for u if we use the worst-case variability for the input, as u > 0.5 normally generates an event, a non-discrete-time Real.

Sorry, my example was bad. What I meant was rather something like this:

model M2b
  function g
    input Real u;
    output Boolean y = u > 0.5;
  end g;
  function f
    input Real u;
    output Boolean y = g(u); /* Needs discrete-time variability for u. */
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2b;

To me the function f would be illegal since g is constructing a non-discrete-time Boolean (regardless of whether the specific function call would work).

Again, the elegant solution to this would be the following:

model M2bFix
  function g
    input Real u;
    output Boolean y = u > 0.5;
  end g;
  function f
    input discrete Real u;
    output Boolean y = g(u); /* Fine; u, and hence g(u), have discrete-time variability. */
    annotation(GenerateEvents = true);
  end f;
  Boolean b = f(1.0);
end M2bFix;

I have a bit of problem seeing the use-case for this; having a function generating events and then declaring that the inputs are discrete mean that function will not generate events. And remember that the simple implementation of GenerateEvents is to inline it, so the case of a function having multiple real inputs - some discrete and some non-discrete - seems quite esoteric.

@henrikt-ma
Copy link
Collaborator

I have a bit of problem seeing the use-case for this; having a function generating events and then declaring that the inputs are discrete mean that function will not generate events. And remember that the simple implementation of GenerateEvents is to inline it, so the case of a function having multiple real inputs - some discrete and some non-discrete - seems quite esoteric.

The point would be to have rules for variability checking that allow for separate checking of the function body and the expressions where the function is called. This is something we (System Modeler) rely heavily upon when GenerateEvents = true is used in combination with InlineAfterIndexReduction = true, since we perform variability checking way ahead of the stage at which inlining due to InlineAfterIndexReduction = true is performed.

Separate variability analysis of the function body also makes the function a much better abstraction, and allows errors to be reported in a simpler way than when the error is a joint effect of the body of the function and the arguments passed.

@HansOlsson
Copy link
Collaborator Author

I have a bit of problem seeing the use-case for this; having a function generating events and then declaring that the inputs are discrete mean that function will not generate events. And remember that the simple implementation of GenerateEvents is to inline it, so the case of a function having multiple real inputs - some discrete and some non-discrete - seems quite esoteric.

The point would be to have rules for variability checking that allow for separate checking of the function body and the expressions where the function is called.

I agree that such a separation is a good thing, but I don't see why it is necessary to declare some inputs as discrete for that.

@HansOlsson HansOlsson marked this pull request as ready for review January 3, 2025 14:22
@HansOlsson HansOlsson added this to the 2025-January milestone Jan 8, 2025
@henrikt-ma
Copy link
Collaborator

The point would be to have rules for variability checking that allow for separate checking of the function body and the expressions where the function is called.

I agree that such a separation is a good thing, but I don't see why it is necessary to declare some inputs as discrete for that.

In the example above,

  function f
    input discrete Real u;
    output Boolean y = g(u); /* Fine; u, and hence g(u), have discrete-time variability. */
    annotation(GenerateEvents = true);
  end f;

It is thanks to f.u being declared discrete that one can separately (and successfully) perform variability checking of f.

If the discrete prefix is removed, variability checking of f(time) would pass (danger, since no event would be generated from g), but one would be saved by the variability checking of f that would reject y = g(u), since only the left-hand side is discrete-time.

@HansOlsson
Copy link
Collaborator Author

The point would be to have rules for variability checking that allow for separate checking of the function body and the expressions where the function is called.

I agree that such a separation is a good thing, but I don't see why it is necessary to declare some inputs as discrete for that.

In the example above,

  function f
    input discrete Real u;
    output Boolean y = g(u); /* Fine; u, and hence g(u), have discrete-time variability. */
    annotation(GenerateEvents = true);
  end f;

It is thanks to f.u being declared discrete that one can separately (and successfully) perform variability checking of f.

But that requires a new deduction rule for discrete variables and expressions. That will in itself require an MCP, so it seems best to first add this PR without it - since this PR fixes the current inconsistency.

If the discrete prefix is removed, variability checking of f(time) would pass (danger, since no event would be generated from g), but one would be saved by the variability checking of f that would reject y = g(u), since only the left-hand side is discrete-time.

With the proposed rule the function f (without discrete) would fail, since g doesn't generate events. To later extend the rules to handle some calls of such a function - seems like a later problem.

@HansOlsson
Copy link
Collaborator Author

model M
  discrete Real a;
  Real b;
equation
  b-2*a=0;
end M;

Poll for adding GenerateEvents-variability so that the following works:

function g
  input Real x;
  output Boolean b;
algorithm
  b:=x>0 ....;
end g; // Ok
model B
  Real x=if g(time) then 1 else 2; // ok, similar as Real x=noEvent(if g(time) then 1 else 2=; 
  Boolean b=g(time); // Not ok
end B;

With GenerateEvents=true: (normally implemented as "simple inlining"):

function g
  input Real x;
  output Boolean b;
algorithm
  b:=x>0 ....;
  annotation(GenerateEvents=true);
end g; // Ok
model B
  Real x=if g(time) then 1 else 2; // ok, generates events; 
  Boolean b=g(time); // Ok, generate events
end B;

Favor: Hans, Francesco, Elena, Gerd, Markus (but unsure about specifics)
Against: Henrik, Anne
Abstain: Dag

@HansOlsson
Copy link
Collaborator Author

This has now been test-implemented in Dymola, without any issues (so the ones that should work do work, and the problematic ones give reasonable diagnostics).

@HansOlsson HansOlsson modified the milestones: 2025-January, 2025-March Mar 5, 2025
@HansOlsson
Copy link
Collaborator Author

HansOlsson commented Mar 6, 2025

@HansOlsson to add examples to pull-request and check rules for inputs to GenerateEvents-functions, and phase and state in Media-library.

@HansOlsson
Copy link
Collaborator Author

@HansOlsson to add examples to pull-request and check rules for inputs to GenerateEvents-functions, and phase and state in Media-library.

I have added examples and checked that those inputs behave as normal variables, so Integer, Boolean, String are discrete-time (which works for things like phase), whereas Real is non-discrete-time. The last two functions is sort of simplified variant of getPhase.

@HansOlsson
Copy link
Collaborator Author

@henrikt-ma to check

@HansOlsson
Copy link
Collaborator Author

@henrikt-ma to check

Ping @henrikt-ma

@henrikt-ma
Copy link
Collaborator

Sorry, this is not a trivial thing to review, so I haven't found the time to do it yet. The good news is that the variability pro @qlambert-pro is back from parental leave, so I have asked him for help.

@henrikt-ma
Copy link
Collaborator

henrikt-ma commented May 7, 2025

@qlambert-pro and I had a look and would like to add two small models for discussion. I'll begin with the more concerning one.

model ShouldBeRejected
  function f
    input Real u;
    output Boolean y = u > 0.5;
  end f;

  function g
    input Boolean u;
    output Boolean y = u;
    annotation(GenerateEvents = true);
  end g;

  Boolean k = g(f(time));
end ShouldBeRejected;

Unless I misread the proposal, this would be accepted:

  • g is making the promise that its Boolean output will be discrete-time due to having GenerateEvents = true, so the declaration equation for k has the required variability.
  • g itself is not being rejected since there is no hint inside of g that u might not be discrete-time.

To fix this, we propose using declared variabilities of the function components:

  function g
    input discrete Boolean u;
    output discrete Boolean y = u;
  end g;

Now, the variability issue is detected in the expression g(f(time)), since f(time) doesn't have the required variability for the argument of g.

Note that GenerateEvents = true isn't even needed in this case, since there are no potentially event-generating expressions in the function.

@henrikt-ma
Copy link
Collaborator

Our second (and less severe) example is the following:

model ShouldBeAccepted
  function f
    input Real u;
    output Real y = if u > 0.5 then 1.0 else 2.0;
    annotation(GenerateEvents = true);
  end f;

  function g
    input Real u;
    output Boolean y = u > 1.5;
  end g;

  Boolean k = g(f(time));
end ShouldBeAccepted;

Our interpretation of the proposed design is that this is rejected due to f(time) being non-discrete-time, resulting in a non-discrete-time Boolean in the declaration equation for k.

Again, declared variability of function inputs and outputs would come to the rescue:

  function f
    input Real u;
    output discrete Real y = if u > 0.5 then 1.0 else 2.0;
    annotation(GenerateEvents = true);
  end f;

One might consider making GenerateEvents = true implicit when a function has one or more outputs declared discrete and one or more inputs not declared discrete, but let's assume it isn't implicit for now.

When GenerateEvents = true, the function definition can be checked for variability errors in the same way as in model context, meaning that the definition of f is accepted since the expression if u > 0.5 then 1.0 else 2.0 has the required discrete-time variability for the variable y.

Without the GenerateEvents = true, the definition of f should be rejected with a variability error in the declaration equation for y, since y is declared to be discrete-time, while the given expression is non-discrete-time due to the invisible noEvent around the relation u > 0.5.

@HansOlsson
Copy link
Collaborator Author

@qlambert-pro and I had a look and would like to add two small models for discussion. I'll begin with the more concerning one.

model ShouldBeRejected
  function f
    input Real u;
    output Boolean y = u > 0.5;
  end f;

  function g
    input Boolean u;
    output Boolean y = u;
    annotation(GenerateEvents = true);
  end g;

  Boolean k = g(f(time));
end ShouldBeRejected;

Unless I misread the proposal, this would be accepted:

I had to re-check, but that was not the intent.
I agree with the following:

  • g is making the promise that its Boolean output will be discrete-time due to having GenerateEvents = true, so the declaration equation for k has the required variability.
  • g itself is not being rejected since there is no hint inside of g that u might not be discrete-time.

To me there are two ways of changing this:

  • Require that the Boolean input to g must be discrete-time, when relevant. I find that complicated (with or without new syntax) due to the 'when relevant'.
  • Change so that the call g() is only discrete-time if non-real inputs are discrete-time.

So basically change:
Function calls where the function has annotation \lstinline!GenerateEvents = true! (\Cref{modelica:GenerateEvents}) and the output does not contain a subtype of \lstinline!Real!.
to:
Function calls where the function has annotation \lstinline!GenerateEvents = true! (\Cref{modelica:GenerateEvents}), the output does not contain a subtype of \lstinline!Real!, and any non-Real inputs have discrete-time variability. (possibly expand "non-Real input" to "any inputs containing a subtype of a base-type other than Real").

The implication is that only the variability of the call is impacted (or non-impacted), but the validity is unchanged.
Thus Real x = if g(f(time)) then 1 else 0; would be ok, as well as Real x = noEvent(if g(f(time)) then 1 else 0);, and it would also kind of work inside a function (except for time, of course).

In practice I could in MSL only find the phase-input as a non-Real input to such a function (for two-phase medium), and region for Water. As far as I understand they are clearly intended to be literal values so I don't see any problem with this restriction. (And I could not find any call where that phase/region was used; and a complication is the two main media deriving from two-phase medium are R134a and Water; both ignore the phase.)

@HansOlsson
Copy link
Collaborator Author

Our second (and less severe) example is the following:

model ShouldBeAccepted
  function f
    input Real u;
    output Real y = if u > 0.5 then 1.0 else 2.0;
    annotation(GenerateEvents = true);
  end f;

  function g
    input Real u;
    output Boolean y = u > 1.5;
  end g;

  Boolean k = g(f(time));
end ShouldBeAccepted;

I don't see this as an issue, and to me the example just looks odd.

Basically I don't see why anyone would use GenerateEvents=true, but then return a discrete-valued value as a Real in an actual model instead of just returning an Integer with those values. Having an integer output would also allow the obvious:

  Integer a=f(time);
  Boolean k=g(a);

The reason for having different rules for Reals and non-Real outputs (and even considering real output from a function generating events) are instead cases such as:

function f
   input Real u;
   output Integer sign;
   output Real absVal;
algorithm
   absVal:=abs(u);
   sign:=if u>0 then 1 else -1;
   annotation(GenerateEvents=true);
end f;

model M
  Integer s=f(time);
  Real x;
equation
  (,x)=f(time);
end M;

(which I believe we cannot yet handle in Dymola).

@HansOlsson HansOlsson modified the milestones: 2025-March, 2025-May May 7, 2025
@HansOlsson HansOlsson requested a review from henrikt-ma May 9, 2025 15:17
input Real x2;
output Boolean b;
algorithm
b := x1 >= x2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Excessive space:

Suggested change
b := x1 >= x2;
b := x1 >= x2;

input Real x2;
output Boolean b;
algorithm
b := x1 >= x2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Excessive space:

Suggested change
b := x1 >= x2;
b := x1 >= x2;

if x2 > 1 then
y := 1;
else
y := x2*x2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have a nice operator for the square:

Suggested change
y := x2*x2;
y := x2^2;

@@ -171,7 +171,7 @@ \section{Function as a Specialized Class}\label{function-as-a-specialized-class}
\item
For initialization of local variables of a function see \cref{initialization-and-binding-equations-of-components-in-functions}).
\item
Components of a function will inside the function behave as though they had discrete-time variability.
Components of a function will inside the function with some exceptions behave as though they had discrete-time variability, see \cref{discrete-time-expressions} for details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Below, it is said that inputs behave as if they were evaluable. Maybe we should just drop this item now that the rules are becoming more complicated?

\item
Unless inside \lstinline!noEvent!: Function calls where the function has annotation \lstinline!GenerateEvents = true! (\Cref{modelica:GenerateEvents}), the output does not contain a subtype of \lstinline!Real!, and any non-\lstinline!Real! inputs have discrete-time variability.
For a function call returning multiple return values (see \cref{output-formal-parameters-of-functions}) the variability is decided separately for each output.
These calls will generate events, \Cref{modelica:GenerateEvents}.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this sentence is more of a distraction than helpful here:

Suggested change
These calls will generate events, \Cref{modelica:GenerateEvents}.

Copy link
Collaborator

@henrikt-ma henrikt-ma left a comment

Choose a reason for hiding this comment

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

I can see now that we are homing in on something that would work.

We need to think about how to deal with the backward incompatibility, though. This function used to be valid, but should be rejected according to the new variability rules:

function f
  input Real x;
  output Boolean b = noEvent(x > 0.5);
  annotation(GenerateEvents = true);
end f;

Of course, the above is just a minimal example. A slightly less minimal but more realistic example might include a call to another function without GenerateEvents = true:

function g
  input Real x;
  output Boolean b = x > 0.5;
end g;
function h
  input Real x;
  output Boolean b = g(x);
  annotation(GenerateEvents = true);
end h;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Indicates that there's a discussion; not clear if bug, enhancement, or working as intended
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants