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

Add variability change for these functions. #3610

Open
wants to merge 7 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).

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