OpenModelica / OpenModelica

OpenModelica is an open-source Modelica-based modeling and simulation environment intended for industrial and academic usage.
https://openmodelica.org
Other
829 stars 305 forks source link

Test of Records with external C Code #7970

Closed SKittan closed 3 years ago

SKittan commented 3 years ago

Description

I've created a small example to test records with external "C" code. Therefor I create a simple record with three Real values in Modelica. The external C function is getting this Record and adding a two on each value.

Steps to Reproduce

Just run the attached example.

Code to reproduce:

Modelica Model:

model structTest
  threeValues abc(a(start=1.), b(start=2.), c(start=3.));

  record threeValues
    Real a, b, c;
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end threeValues;

  function add
    output threeValues abc;

    external "C"
      add_c(abc) annotation(
      IncludeDirectory = "modelica://structTest",
      Include = "#include \"add_two.c\"");
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end add;
algorithm
  abc := add();
  annotation(
    Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
    Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
end structTest;

C-File:

#include <stdio.h>

typedef struct
{
    double a, b, c;
}threeValues;

void add_c(void*);

void add_c(void* in){
    threeValues* out = (threeValues*)in;

    //printf("In Pointer Address: %i\n", in);

    out->a += 2.;
    out->b += 2.;
    out->c += 2.;
}

Expected Behavior

I would expect, that the initial values raise each time step by two. But the values in Modelica are nan (see attached Screenshot). Additionally I tracked the Pointer address of the c struct via print function. I can see, that this address is changing. I didn't expect this, since the Modelica Documentation says, that records are handed over by reference. Hence I would assume, that the record data is just created once and then held by Modelica (without any copies).

A side effect of the print function is, that the data displayed by Modelica is two for each value and time step. Is this an indicate that a new struct is created in every time step and initialised with 0?

Screenshots

grafik

Results with activated print grafik grafik

Version and OS

perost commented 3 years ago

12.9.3 in the specification says:

An external function is not allowed to internally change the inputs (even if they are restored before the end of the function)

The specification does say that the record is passed by reference, but that doesn't say anything about the lifetime of the record that is passed to the function.

If you need to store internal memory between calls to the external function you might instead want to use an external object.

adrpo commented 3 years ago

Another way would be to return a new record from the external function.

sjoelund commented 3 years ago

The specification does say that the record is passed by reference, but that doesn't say anything about the lifetime of the record that is passed to the function.

This is an output though. So it is returned. The problem is outputs are not initialized even if you add an initialization to the record:

  structTest_threeValues_construct(threadData, _abc);
  tmp2._a = 3.0;
  tmp2._b = 4.0;
  tmp2._c = 5.0;
  tmp1 = tmp2;
  structTest_threeValues_copy(tmp1, _abc);;

  add_c(&_abc_ext); // _abc is not passed here !!!
  _abc = (structTest_threeValues)_abc_ext;
SKittan commented 3 years ago

I had problems returning the data as new record. My solution for now is to manage my data in the C functions via malloc and pointer.

#pragma once
#include <stdlib.h>

typedef struct
{
    double a, b, c;
}threeValues;

size_t init_data(double, double, double);
void add_c(size_t);
double get_a(size_t);
double get_b(size_t);
double get_c(size_t);
void free_data(size_t);

size_t init_data(double a, double b, double c){
    threeValues* abc = (threeValues*)malloc(sizeof(threeValues));
    abc->a = a;
    abc->b = b;
    abc->c = c;

    return (size_t)abc;
}

void add_c(size_t in){
    threeValues* out = (threeValues*)in;

    out->a += 2.;
    out->b += 2.;
    out->c += 2.;
}

double get_a(size_t ptData){
    threeValues* data = (threeValues*)ptData;
    return data->a;
}

double get_b(size_t ptData){
    threeValues* data = (threeValues*)ptData;
    return data->b;
}

double get_c(size_t ptData){
    threeValues* data = (threeValues*)ptData;
    return data->c;
}

void free_data(size_t ptData){
    threeValues* data = (threeValues*)ptData;
    free(data);
}
casella commented 3 years ago

Another way would be to return a new record from the external function.

This could cause huge memory leaks if the memory for the new record is malloc'd each time the function is called.

My understanding from the old days when we wrote ExternalMedia is that record (and array) external function outputs are passed by reference, meaning that the simulation runtime allocates the memory and passes a pointer to the function, that uses the pointer to fill it in.

The only case when this doesn't work is for String outputs, because the amount of memory is not known a priori. In this case the Specification define ad-hoc functions to be called, see Section 12.9.6.1. In this case the Specification specifically advises not to use malloc, because a Modelica environment may have a different memory allocation mechanism, e.g. stack-based.

Do I miss something?

casella commented 3 years ago

@SKittan, in your example, the Modelica function add() has only an output, no inputs. So, you can't pass any value to it, to increase them by some amounts. Modelica functions are memoryless. If you want to achieve that semantics, you need to write something like

model structTest
  threeValues abc(a(start=1.), b(start=2.), c(start=3.));

  record threeValues
    Real a, b, c;
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end threeValues;

  function add
    input threeValues abc_in;
    output threeValues abc;

    external "C"
      add_c(abc_in, abc) annotation(
      IncludeDirectory = "modelica://structTest",
      Include = "#include \"add_two.c\"");
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end add;
algorithm
  abc := add(abc_in);
  annotation(
    Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
    Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
end structTest;

and the C code should be something like

#include <stdio.h>

typedef struct
{
    double a, b, c;
}threeValues;

void add_c(threeValues*, threeValues*);

void add_c(threeValues* in, threeValues* out){
    //printf("In Pointer Address: %i\n", in);

    out->a = in->a+2;
    out->b = in->b+2;
    out->c = in->c+2;
}
SKittan commented 3 years ago

Thank you for your remark. For now I use the malloc only in the init function. This is invoked only once during initialization phase of a simulation. So from my understanding there should be no memory leak:

...
algorithm
  if initial() then // Construct
    pt2abc := init(abc);
  elseif terminal() then // Destruct
    free(pt2abc);
  else // Calculate
    add(pt2abc);
  end if;
    abc.a := getA(pt2abc);
    abc.b := getB(pt2abc);
    abc.c := getC(pt2abc);
  annotation(
    Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
    Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
end structTest;

But I prefer a solution, where Modelica manages the memory. Hence, I will try your recommendation.

sjoelund commented 3 years ago

You shouldn't need to have a malloc in the unit function either. Just pass the output by reference as an input argument to the function and fill in the values.

SKittan commented 3 years ago

I have now this variant:

model structTest
  threeValues abc(a(start=1.), b(start=2.), c(start=3.));

  record threeValues
    Real a, b, c;
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end threeValues;

  function add
    input threeValues abc_in;
    output threeValues abc_out;

    external "C"
      add_c(abc_in, abc_out) annotation(
      IncludeDirectory = "modelica://structTest",
      Include = "#include \"add_two.c\"");
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end add;
algorithm
  abc := add(abc);
  annotation(
    Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
    Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
end structTest;

With the C-Code:

typedef struct
{
    double a, b, c;
}threeValues;

void add_c(void*, void*);

void add_c(void* in, void* out){
    threeValues* in_typed = (threeValues*)in;
    threeValues* out_typed = (threeValues*)out;

    out_typed->a = in_typed->a + 2.;
    out_typed->b = in_typed->b + 2.;
    out_typed->c = in_typed->c + 2.;
}

The Simulation result (1s with 500 steps): grafik

casella commented 3 years ago

@SKittan, this result is expected. Remember that Modelica is a declarative equation-based modelling language, not an imperative language like C or Python. A continuous-time Modelica model (with no events) as the one you wrote is conceptually equivalent to a bunch of equations. Even algorithms in models are conceptually equivalent to equations. There is absolutely no concept of "time step" in there.

For continuous variables, as abc is in your example, the specification mandates that each time the algorithm is executed, the left-hand-side variables are always initialized to their start values, precisely in order to avoid introducing unwanted memory effects. If you want memory, you need to use discrete variables and when-equations, see below.

This means that at each time step, abc is set to {1,2,3}, then each component is added a value of 2, so you always get the same result.

If you want to have some discrete-time dynamics, you have to describe it explicitly with discrete variables and when-equations, that are triggered at some event times, e.g.

model structTest
  discrete threeValues abc(a(start=1.), b(start=2.), c(start=3.));

  record threeValues
    Real a, b, c;
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end threeValues;

  function add
    input threeValues abc_in;
    output threeValues abc_out;

    external "C"
      add_c(abc_in, abc_out) annotation(
      IncludeDirectory = "modelica://structTest",
      Include = "#include \"add_two.c\"");
    annotation(
      Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
      Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
  end add;
algorithm
  when sample(0,0.1) then
    abc := add(abc);
  end when;
  annotation(
    Icon(coordinateSystem(extent = {{-200, -200}, {200, 200}})),
    Diagram(coordinateSystem(extent = {{-200, -200}, {200, 200}})));
end structTest;

Now abc is a discrete variable, so the rules for algorithm semantics are different: each time the when clause is triggered, all LHS variables are initialized with their pre() value, i.e. the value immediately before the event is processed. Every 0.1 seconds an event is triggered, and abc is updated based on the previous value.

Of course events can also be triggered by other means than sample(), any boolean condition can be put in the when statement.

I guess this was what you were looking for?

SKittan commented 3 years ago

@casella Thank you for this explination. I think with this information I can adapt the test case to my real application.

casella commented 3 years ago

If you need more help feel free to use this ticket. I closed it for our records, since there is nothing wrong with OMC as I understand.