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

Synchronised variable transfer #189

Merged
merged 5 commits into from
Mar 15, 2019

Conversation

kyllingstad
Copy link
Member

This fixes issue #181, "Only transfer variable values at common communication points".

Please have a good look at this and give it some consideration before approving, as it's a bit difficult to verify with tests that this is the right way to go about it. (Fixing #186 would help.)

This fixes issue #181, "Only transfer variable values at common
communication points".
@kyllingstad kyllingstad added the bug Something isn't working label Feb 25, 2019
@kyllingstad kyllingstad self-assigned this Feb 25, 2019
@eidekrist
Copy link
Member

Sounds like it would be quite straightforward to verify this functionality with a test? For example two mock slaves with decimation factors of 2 and 3, connect real out->real in, verify that values are transferred every 6 base steps. Or are you saying such a test would not be sufficient?

@mrindar
Copy link
Contributor

mrindar commented Feb 26, 2019

What happens here if we have slaves: A, B and C, where slave A and B both have one output connected to an input of slave C, and all of them have different decimation factors? In this case I suppose we want to step slave C only when A, B and C is at a common communication point? Also, is there any reason to transfer variables if a step is not going to be perfomed?

@kyllingstad
Copy link
Member Author

Sounds like it would be quite straightforward to verify this functionality with a test?

I was thinking more like verifying that it doesn't have subtle unwanted consequences for the simulation results, but you are right, just testing the basic functionality should be simple enough. And it should be done, so I'll add a test for it!

@kyllingstad
Copy link
Member Author

kyllingstad commented Feb 26, 2019

What happens here if we have slaves: A, B and C, where slave A and B both have one output connected to an input of slave C, and all of them have different decimation factors? In this case I suppose we want to step slave C only when A, B and C is at a common communication point?

This has nothing to do with when to step, that is controlled solely by the step size and decimation factor of each individual slave. This only has to do with when we transfer variable values. And with the change proposed here, we transfer from A to C whenever they have a common communication point, and from B to C whenever they have a common communication point.

Also, is there any reason to transfer variables if a step is not going to be perfomed?

Not really, but it doesn't harm either, and it's easier to just update the variables than to special-case it. It probably won't affect performance that much, because it only updates the local cache. Nothing is actually sent to the slave until do_step() is called anyway.

@mrindar
Copy link
Contributor

mrindar commented Feb 26, 2019

This has nothing to do with when to step, that is controlled solely by the step size and decimation factor of each individual slave. This only has to do with when we transfer variable values. And with the change proposed here, we transfer from A to C whenever they have a common communication point, and from B to C whenever they have a common communication point.

I suppose this is OK, but when would C be stepped in this scenario? It is my understanding that slave C should only be stepped at time t_k, when all the inputs have valid values for time t_k. If this is the case, then C should only be stepped at the time point which is common for all slaves conntected to its inputs, in this case slave A and B.

@ssadjina
Copy link

I think it helps to ask "when must a connection/bond step?" and not "when must a slave step?". It actually makes little sense IMHO to talk about when one slave should step/synchronize, it always is a matter of when a connection/bond should "step" and synchronize the slaves it connects.
So in @mrindar 's example I guess one should really define a step size for bond A-C and one for B-C and then follow the logic:

  1. A steps when A-C steps.
  2. B steps when B-C steps.
  3. C steps whenever A-B or A-C steps.

Thoughts?

@mrindar
Copy link
Contributor

mrindar commented Feb 26, 2019

3\. C steps whenever A-B or A-C steps.

This means that at a given doStep for C, the input values will not be synchronized for the given time point. Which can not possibly be correct(?)

@kyllingstad
Copy link
Member Author

kyllingstad commented Feb 26, 2019

I suppose this is OK, but when would C be stepped in this scenario? It is my understanding that slave C should only be stepped at time t_k, when all the inputs have valid values for time t_k.

In my opinion, this is not the case. C should be stepped at the rate that the user has set for it. If there are no new values for one or more of its inputs, those inputs should simply not be set, and the slave should be free to handle this however it sees fit (e.g. by extrapolation from previous values).

I base this on two observations (or rather lack thereof) of things that are not mentioned in the FMI standard:

  1. There is no rule in FMI stating that fmi2SetXxx() must be called for all inputs at all communication points.
  2. It is not specified that the slave must use the old value for an input variable if it doesn't receive a new one.

@kyllingstad
Copy link
Member Author

If there are no new values for one or more of its inputs, those inputs should simply not be set

Note that this is currently not what happens; the inputs always get set due to the way we do caching. This is what @hplatou mentioned in today's meeting.

@kyllingstad
Copy link
Member Author

If there are no new values for one or more of its inputs, those inputs should simply not be set, and the slave should be free to handle this however it sees fit (e.g. by extrapolation from previous values).

More concisely, the following (pseudo-code):

slave.set_input(123);
slave.do_step(1.0);
slave.do_step(1.0);

should be equivalent to this:

slave.set_input(123);
slave.do_step(2.0);

@mrindar
Copy link
Contributor

mrindar commented Feb 26, 2019

Aah, ok. I thought we agreed on the exact opposite in the previous mob session xD

Copy link
Contributor

@hplatou hplatou left a comment

Choose a reason for hiding this comment

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

This looks good to me.

@kyllingstad
Copy link
Member Author

Aah, ok. I thought we agreed on the exact opposite in the previous mob session xD

Well, any disagreements definitely need to be laid on the table now. I encourage you all to double-check the relevant parts of the FMI (2.0) spec and give me your interpretations of it. I may very well be wrong in this, I don't know it all by heart even if I sometimes pretend to. ;)

PS: I accidentally edited @mrindar's post to write this reply, instead of starting a new comment. Apologies, and reverted now.

@mrindar
Copy link
Contributor

mrindar commented Feb 26, 2019

From the FMI 2.0 standard:

FMI for Co-Simulation defines interface routines for the communication between the master and all
slaves (subsystems) in a co-simulation environment. The most common master algorithm stops at each
communication point tci the simulation (time integration) of all slaves, collects the outputs y(tci) from all
subsystems, evaluates the subsystem inputs u(tci), distributes these subsystem inputs to the slaves and
continues the (co-)simulation with the next communication step tci → tci+1 = tci+ hc with fixed
communication step size hc. In each slave, an appropriate solver is used to integrate one of the
subsystems for a given communication step tci → tci+1. The most simple co-simulation algorithms
approximate the (unknown) subsystem inputs u(t), (t > tci) by frozen data u(tci) for tci ≤ t < tci+1
. FMI for
Co-Simulation supports this classical brute force approach as well as more sophisticated master
algorithms. FMI for Co-Simulation is designed to support a very general class of master algorithms but it
does not define the master algorithm itself.

I interpret this to mean that it is the master algorithms responsibility to ensure 'input correctness'. So if we have the following stepping scenario:

A: |a1|---|a2|---|a3|---|a4|---|a5|
B: |b1|------------------------|b2|
C: |c1|----------|c2|----------|c3|

where A and B have outputs connected to C, the master algorithm has to decide what to set as input values in |c2|. One possibility is to simply use |a3| and |b1|. Another approach is to use |a3| and a linear interpolation of |b1| and |b2|. More complicated interpolation methods could also be used if the slaves provide derivative information.

Thoughts?

Edit:
Sorry for the insane amount of edits. Formatting is hard!

Edit Edit:
Wait wait wait, I'm being a complete dumbo here. You can't interpolate |b1| and |b2|of course!

@kyllingstad
Copy link
Member Author

I interpret this to mean that it is the master algorithms responsibility to ensure 'input correctness'.

I don't. As this is a standards document, and hence must be expected to be precise, I don't think we should interpret anything into it that isn't explicitly stated. And I don't see it stated that the master algorithm must provide all inputs, only that it may do so.

So if we have the following stepping scenario:

A: |a1|---|a2|---|a3|---|a4|---|a5|
B: |b1|------------------------|b2|
C: |c1|----------|c2|----------|c3|

where A and B have outputs connected to C, the master algorithm has to decide what to set as input values in |c2|.

Consider the connection from B to C, and assume that B sends derivatives with its outputs. Hey, that is awesome, now C can extrapolate its corresponding input between communication points for improved accuracy. However, if a simplistic master algorithm then goes and "resets" the value of the input at c2 to the b1 value, then that improvement is lost.

@mrindar
Copy link
Contributor

mrindar commented Mar 1, 2019

I yield ¯\_(ツ)_/¯

@ssadjina
Copy link

ssadjina commented Mar 2, 2019

As stated, in the most simple case (0th order interpolation) you would just hold the last input and continue to use it unless an updated one becomes available. I think both of @mrindar and @kyllingstad 's latest comments are correct as far as I can tell.

Just an academic side note (not really important):

Edit Edit:
Wait wait wait, I'm being a complete dumbo here. You can't interpolate |b1| and |b2|of course!

No, they actually could be in an iterative scheme or when first stepping B, and then stepping C (slaves don't have to step in parallel in all cases).

@kyllingstad
Copy link
Member Author

I am working both on a test for this as well as a fix for the caching issue, so I'll close this for now. I'll reopen when the fix is ready to merge.

@kyllingstad kyllingstad closed this Mar 5, 2019
@kyllingstad
Copy link
Member Author

I started changing things so that we only set the input variables where we have received an updated value (as opposed to setting all connected inputs at all time steps). But I've hit a snag and would like some feedback:

We now have the ability to set manipulators functions on input variables. These can be used to modify incoming values. However, we also use them in scenarios, where we use the "override" functionality to set values of unconnected input variables. With my planned change, this will no longer work. An unconnected input variable will not receive any values, and an override will therefore no longer have an effect.

I see two solutions:

  • State that functions on input variables only modify incoming values, and if there are no incoming values, there will be no modified value either. Find some other solution for scenarios.
  • Create a special case where we always run functions on input variables that have them, even if there are no incoming values.

Any comments or preferences here?

@hplatou
Copy link
Contributor

hplatou commented Mar 6, 2019

That’s a good question. I think that the ability to override unconnected inputs is required so we need to find some solution for that. In that sense I think I will be voting for the second solution.

@kyllingstad
Copy link
Member Author

I believe I have fixed the caching issue now, so I am reopening. This required separating the "get cache" and "set cache" into two different helper classes in slave_simulator, because they now do caching and manipulation differently.

I've also added a test that verifies the functionality.

@kyllingstad kyllingstad reopened this Mar 10, 2019
@kyllingstad kyllingstad force-pushed the bugfix/181-only-transfer-values-at-synch-points branch from f134121 to e04e26e Compare March 10, 2019 22:06
This ensures that only the variables that have actually been set() will
be cached in `slave_simulator` and passed to
`async_slave::set_variables()`.  Previously, all *exposed* variables
would always be in the cache.
This test verifies that issue #181 has been fixed, i.e., that variable
values are only transfered between simulators at common synchronisation
points.
@kyllingstad kyllingstad force-pushed the bugfix/181-only-transfer-values-at-synch-points branch from e04e26e to 9c78d76 Compare March 10, 2019 22:08
@kyllingstad kyllingstad requested review from hplatou and mrindar March 10, 2019 22:08
@kyllingstad
Copy link
Member Author

I think that the ability to override unconnected inputs is required so we need to find some solution for that. In that sense I think I will be voting for the second solution.

I've made it so that when a manipulator is set for a variable, it will always be run on the last set value for that variable. If no value has been set yet, it will use the default value.

Copy link
Contributor

@hplatou hplatou left a comment

Choose a reason for hiding this comment

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

Nice! This looks good to me.

@eidekrist
Copy link
Member

I've made it so that when a manipulator is set for a variable, it will always be run on the last set value for that variable. If no value has been set yet, it will use the default value.

From reading the code, it seems the manipulator will only run once, and after that only if/when a new value is set with set_value(). So for parameters and unconnected inputs, a manipulator will only run once, which may be a problem for future stateful manipulators such as noise or ramps.

Copy link
Member

@eidekrist eidekrist left a comment

Choose a reason for hiding this comment

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

While I like the updated test, I am missing a test case for the original predicament leading to this PR;
A fast slave has an input connected to the output from a slow slave, and we want to verify that the fast slave only gets its input updated at "matching" steps. Right now we only test the opposite case (fast -> slow), but that might be sufficient?

// specifies whether they have been run on the values currently in
// `values_`.
std::unordered_map<variable_index, std::function<T(T)>> manipulators_;
bool hasRunManipulators_ = false;
Copy link
Member

Choose a reason for hiding this comment

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

Is hasRunManipulators_ mainly a guard against setting values and manipulators during slave_simulator::set_variables()?

Copy link
Member Author

@kyllingstad kyllingstad Mar 11, 2019

Choose a reason for hiding this comment

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

Partly, but that's not its primary purpose. It is used in set_variable_cache::manipulate_and_get() to check whether the manipulators have already been run, to avoid running them multiple times on the same values if the function is called again. (That never happens now, but could easily happen in the future.)

Compare with how it was done before, and still is in get_variable_cache, where you first call run_manipulators() and then access the manipulatedValues vector directly. In the new class, I've made the member variables private and tried to offer a less error prone interface.

@ljamt
Copy link
Member

ljamt commented Mar 11, 2019

From reading the code, it seems the manipulator will only run once, and after that only if/when a new value is set with set_value(). So for parameters and unconnected inputs, a manipulator will only run once, which may be a problem for future stateful manipulators such as noise or ramps.

Can we use the ramp as a case? Will it be sufficient that the "manipulator-function" takes delta-t as an argument?

@kyllingstad
Copy link
Member Author

From reading the code, it seems the manipulator will only run once, and after that only if/when a new value is set with set_value().

Correct.

So for parameters and unconnected inputs, a manipulator will only run once, which may be a problem for future stateful manipulators such as noise or ramps.

Dang, you're right. I didn't think of that use case at all. Good catch!

Possible solutions, off the top of my head:

  1. Always update at each time step when a manipulator is present. (Simplest, but feels a bit crude.)
  2. Add a parameter to simulator::set_xxx_manipulator() that allows client code to specify whether the value should be updated at each time step or only when there are incoming values. (Does the client code always know which to choose, though?)
  3. Add some machinery in both algorithm and simulator implementations whereby an algorithm can tell a simulator whether each of its variables is connected. (Overengineered?)

@kyllingstad
Copy link
Member Author

While I like the updated test, I am missing a test case for the original predicament leading to this PR;
A fast slave has an input connected to the output from a slow slave, and we want to verify that the fast slave only gets its input updated at "matching" steps.

I agree. I'll add a test.

Right now we only test the opposite case (fast -> slow), but that might be sufficient?

Technically, it's sufficient to test the current implementation because lcm(a, b) == lcm(b, a) for all a, b. But that implementation could change, so a test is definitely in order.

@kyllingstad
Copy link
Member Author

Can we use the ramp as a case? Will it be sufficient that the "manipulator-function" takes delta-t as an argument?

The manipulator functions don't get the current time, nor the step size, at the moment, so more changes are needed to support that case.

@eidekrist
Copy link
Member

Possible solutions, off the top of my head:

  1. Always update at each time step when a manipulator is present. (Simplest, but feels a bit crude.)
  2. Add a parameter to simulator::set_xxx_manipulator() that allows client code to specify whether the value should be updated at each time step or only when there are incoming values. (Does the client code always know which to choose, though?)
  3. Add some machinery in both algorithm and simulator implementations whereby an algorithm can tell a simulator whether each of its variables is connected. (Overengineered?)

This is starting to feel like a design choice. Until we hash out a master plan for the intended usage of these manipulator functions - i.e. how often will they be called, how good of an idea is statefulness etc., and as long as there are no current stateful manipulator function implementations, I'm OK with tackling this in a future issue.

The manipulator functions don't get the current time, nor the step size, at the moment, so more changes are needed to support that case.

Another input to this debate is the cse::manipulator class, which gets notified each time a step commences. This could also be taken advantage of when it comes to time dependent manipulations.

@kyllingstad
Copy link
Member Author

While I like the updated test, I am missing a test case for the original predicament leading to this PR;
A fast slave has an input connected to the output from a slow slave, and we want to verify that the fast slave only gets its input updated at "matching" steps.

I agree. I'll add a test.

Done.

Copy link
Member

@eidekrist eidekrist left a comment

Choose a reason for hiding this comment

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

Looks good to me! There was a seemingly random test failure on Jenkins which had me worried, so we might have to to keep our eyes open here 😃

@kyllingstad
Copy link
Member Author

Looks good to me! There was a seemingly random test failure on Jenkins which had me worried, so we might have to to keep our eyes open here 😃

Hm, that is worrisome indeed. And given that it fails in exactly the same way on two different platforms makes it seem less random. I'll look into it a bit more before merging the PR.

@kyllingstad
Copy link
Member Author

Found the problem: #213

It is really unrelated to this PR, so I'll just go ahead and merge now. (It was detected by an assert call in this PR, though, so yay for assert!)

@kyllingstad kyllingstad merged commit ed49ed4 into master Mar 15, 2019
@kyllingstad kyllingstad deleted the bugfix/181-only-transfer-values-at-synch-points branch March 15, 2019 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants