diff --git a/doc/spec_dataValidityPropagation.dox b/doc/spec_dataValidityPropagation.dox index d8136512769cb6c20f7348128384a68f8a26ef16..d3a9bc17f14be0357d8b25081a1863e036842745 100644 --- a/doc/spec_dataValidityPropagation.dox +++ b/doc/spec_dataValidityPropagation.dox @@ -9,8 +9,10 @@ namespace ChimeraTK { > **NOTICE FOR FUTURE RELEASES: AVOID CHANGING THE NUMBERING!** The tests refer to the sections, incl. links and unlinked references from tests or other parts of the specification. These break, or even worse become wrong, when they are not changed consistenty! 1. General idea ---------------- +--------------------------------------- +\anchor dataValidity_1 + - 1.1 In ApplicationCore each variable has a data validiy flag attached to it. DataValidity can be 'ok' or 'faulty'. - 1.2 This flag is automatically propagated: If any of the inputs of an ApplicationModule is faulty, the data validity of the module becomes faulty, which means all outputs of this module will automatically be flagged as faulty. @@ -35,13 +37,13 @@ all outputs of this module will automatically be flagged as faulty. - 2.1.2 The decorator knows about the module it is connected to. It is called the 'owner'. - 2.1.3 **read:** For each read operation it checks the incoming data validity and increases/decreases the data fault counter of the owner. -- 2.1.5 **write:** When writing, the decorator is checking the validity of the owner and the individual flag of the output set by the user. Only if both are 'ok' the output validity is 'ok', otherwise the outgoing data is send as 'faulty'. +- \anchor dataValidity_2_1_5 2.1.5 **write:** When writing, the decorator is checking the validity of the owner and the individual flag of the output set by the user. Only if both are 'ok' the output validity is 'ok', otherwise the outgoing data is send as 'faulty'. ### 2.2 removed ### 2.3 ApplicationModule -- 2.3.1 Each ApplicationModule has one data fault counter variable which is increased/decreased by EntityOwner::incrementDataFaultCounter() and EntityOwner::decrementDataFaultCounter. +- \anchor dataValidity_2_3_1 2.3.1 Each ApplicationModule has one data fault counter variable which is increased/decreased by EntityOwner::incrementDataFaultCounter() and EntityOwner::decrementDataFaultCounter. - 2.3.2 All inputs and outputs have a MetaDataPropagatingRegisterDecorator. - \anchor dataValidity_2_3_3 2.3.3 The main loop of the module usualy does not care about data validity. If any input is invalid, all outputs are automatically invalid \ref dataValidity_comment_2_3_3a "(*)". The loop just runs through normaly, even if an input has invalid data. \ref dataValidity_comment_2_3_3b "(*)" - 2.3.4 Inside the ApplicationModule main loop the module's data fault counter is accessible. The user can increment and decrement it, but has to be careful to do this in pairs. The more common use case will be to query the module's data validity. @@ -85,6 +87,49 @@ See @ref spec_execptionHandling. - 3.1 The decorators which manipulate the data fault counter are responsible for counting up and down in pairs, such that the counter goes back to 0 if all data is ok, and never becomes negative. +4. Circular dependencies +------------------------ + +If modules have circular dependencies, the algorithm described in \ref dataValidity_1 "section 1" leads to a self-excited loop: Once the DataValidity::invalid flag has made a full circle, there is always at least on input with invalid data in each module and you can't get rid of it any more. To break this circle, the following additional behaviour is implemented: + +### 4.1 General behaviour + +- \anchor dataValidity_4_1_1 4.1.1 Inputs which are part of a circular dependency are marked as _circular input_.[\ref dataValidity_test_TestCircularInputDetection "T"] + - \anchor dataValidity_4_1_1_1 4.1.1.1 Inputs which are coming from other applicatiation modules + which are not part of the circle, from the control system module or from device modules are considered _external inputs_.[\ref dataValidity_test_TestCircularInputDetection "T" (only CS module)] +- \anchor dataValidity_4_1_2 4.1.2 All modules which have a circular dependency form a _circular network_.[\ref dataValidity_test_TestCircularInputDetection "T"] + - \anchor dataValidity_4_1_2_1 4.1.2.1 Also entangled circles of different variables which intersect in some of the modules are part of the same circular network.[\ref dataValidity_test_TestCircularInputDetection2 "T"] + - \anchor dataValidity_4_1_2_2 4.1.2.2 There can be multiple disconnected circular networks in an application.[\ref dataValidity_test_TestCircularInputDetection2 "T"] +- 4.1.3 Circular inputs and circular networks are identified at application start after the variable networks are established. +- \anchor dataValidity_4_1_4 4.1.4 As long as at least one _external input_ of any module in the _circular network_ is invalid, the invalidity flag is propagated as described in + \ref dataValidity_1 "1." [\ref dataValidity_test_OneInvalidVariable "T"] +- \anchor dataValidity_4_1_5 4.1.5 Once all _external inputs_ of one _circular network_ are back to DataValidity::ok, all _circular inputs_ of the _circular network_ ignore the +invalid flag and also switch to DataValidity::ok. This breaks the self-exciting loop. [\ref dataValidity_test_OneInvalidVariable "T"] [\ref dataValidity_test_TwoFaultyInOneModule "T"] +- \anchor dataValidity_4_1_6 4.1.6 If all inputs of a module have DataValidity::ok, the module's output validity is also DataValidity::ok, even if other modules in +the _circular network_ have _external inputs_ which are invalid. +- \anchor dataValidity_4_1_7 4.1.7 The ControlSystemModule and DeviceModules are never part of a circular dependency.[\ref dataValidity_test_TestCircularInputDetection2 "T" (only CS module)] +- \anchor dataValidity_4_1_8 4.1.8 If the user code of a module progammatically sets one of its outpts to `faulty`, this is treated as if an _external input_ was invalid.[\ref dataValidity_test_outputManuallyFaulty "T" (only CS module)] + +### 4.2 Side effects and race conditions + +- \anchor dataValidity_4_2_1 4.2.1 The data validity of circular inputs is ignored as soon as all external inputs are back to `ok`, even if the data with the invalidity flag has not been propagated in the whole circle yet. (The data validity is set to `ok` "too early") +- 4.2.2 If it is strictly required that a DataValidity::faulty flag is always transported because critical decisions depend on it, circular dependencies must be avoided in the application. +- \anchor dataValidity_4_2_3 4.2.3 When an _external input_ changes validity from `faulty` to `ok`, the invalidity at the _circular inputs_ of that module might or might not be ignored, depending on the other _external inputs_ of the _circular network_. In large, entangled networks, where the remaining faulty _external input_ is "far away", this might give the impression the the invalidity is resovled "too late". However, it is consistent. Only if ALL external inputs in the _circular network_ are `ok`, the self-exciting loop may be broken. [\ref dataValidity_test_TwoFaultyInTwoModules "T"] + +### 4.3 Technical implementation + +- \anchor dataValidity_4_3_1 4.3.1 In addition to the owner (see \ref dataValidity_2_3_1 "2.3.1"), the _circular network_ (\ref dataValidity_4_1_2 "4.1.2") also gets an atomic invalidity counter.(\ref dataValidity_comment_4_3_1 "*"). +- \anchor dataValidity_4_3_2 4.3.2 Each module and each _circular input_ knows its _circular network_ [\ref dataValidity_test_TestCircularInputDetection2 "T"] +- 4.3.3 If an _external input_ receives data, it increases/decreases the _circular network_'s invalidity counter, together with the owner's invaliditys counter. +- 4.3.4 If a module estimates its validity (as used in \ref dataValidity_2_1_5 "2.1.5"), it returns + - `DataValidity::ok` if the module's internal invalidity counter is 0 (\ref dataValidity_4_1_6 "4.1.6") + - `DataValidity::ok` if the module's internal invalidity counter is not 0 and the _circular network_'s invalidity counter is 0 (\ref dataValidity_4_1_5 "4.1.5") + - `DataValidity::faulty` if both counters are not 0 (\ref dataValidity_4_1_4 "4.1.4") + +#### Comments + +- \anchor dataValidity_comment_4_3_1 \ref dataValidity_4_3_1 "4.3.1" This counter has to be atomic because it is accessed from different module threads. + */ } // end of namespace ChimeraTK diff --git a/include/Application.h b/include/Application.h index f081f0514beea11ea99a3c31302a05673dd0f674..cc8be74b3b3491c88b61c7d01a16503b56b3c01e 100644 --- a/include/Application.h +++ b/include/Application.h @@ -478,6 +478,10 @@ namespace ChimeraTK { template<typename UserType> friend class DebugPrintAccessorDecorator; // needs access to the idMap + template<typename UserType> + friend class MetaDataPropagatingRegisterDecorator; // needs to access circularNetworkInvalidityCounters + friend class ApplicationModule; // needs to access circularNetworkInvalidityCounters + VersionNumber getCurrentVersionNumber() const override { throw ChimeraTK::logic_error("getCurrentVersionNumber() called on the application. This is probably " "caused by incorrect ownership of variables/accessors or VariableGroups."); @@ -502,6 +506,10 @@ namespace ChimeraTK { throw ChimeraTK::logic_error("getInputModulesRecursively() called on the application. This is probably " "caused by incorrect ownership of variables/accessors or VariableGroups."); } + size_t getCircularNetworkHash() override { + throw ChimeraTK::logic_error("getCircularNetworkHash() called on the application. This is probably " + "caused by incorrect ownership of variables/accessors or VariableGroups."); + } }; } /* namespace ChimeraTK */ diff --git a/include/ApplicationModule.h b/include/ApplicationModule.h index 08634d7145d0bf49a0811884f6685f701f138d76..645ada81c072c6840d388c92627a744c07b6b970 100644 --- a/include/ApplicationModule.h +++ b/include/ApplicationModule.h @@ -13,6 +13,8 @@ #include <boost/thread.hpp> #include "ModuleImpl.h" +#include "Application.h" +#include "CircularDependencyDetectionRecursionStopper.h" namespace ChimeraTK { @@ -71,9 +73,7 @@ namespace ChimeraTK { VersionNumber getCurrentVersionNumber() const override { return currentVersionNumber; } - DataValidity getDataValidity() const override { - return (dataFaultCounter == 0) ? DataValidity::ok : DataValidity::faulty; - } + DataValidity getDataValidity() const override; void incrementDataFaultCounter() override; void decrementDataFaultCounter() override; @@ -84,6 +84,13 @@ namespace ChimeraTK { std::list<EntityOwner*> getInputModulesRecursively(std::list<EntityOwner*> startList) override; + size_t getCircularNetworkHash() override; + + /** Set the ID of the circular dependency network. This function can be called multiple times and throws if the + * value is not identical. + */ + void setCircularNetworkHash(size_t circularNetworkHash); + protected: /** Wrapper around mainLoop(), to execute additional tasks in the thread * before entering the main loop */ @@ -98,6 +105,14 @@ namespace ChimeraTK { /** Number of inputs which report DataValidity::faulty. */ size_t dataFaultCounter{0}; + + /** Unique ID for the circular dependency network. 0 if the EntityOwner is not in a circular dependency network. */ + size_t _circularNetworkHash{0}; + + /** Helper needed to stop the recusion when detecting circular dependency networks. + * Only used in the setp phase. + */ + detail::CircularDependencyDetectionRecursionStopper _recursionStopper; }; } /* namespace ChimeraTK */ diff --git a/include/CircularDependencyDetectionRecursionStopper.h b/include/CircularDependencyDetectionRecursionStopper.h new file mode 100644 index 0000000000000000000000000000000000000000..f8fd080c8702028bd6ad2eb785a71307ae771b46 --- /dev/null +++ b/include/CircularDependencyDetectionRecursionStopper.h @@ -0,0 +1,37 @@ +#ifndef CIRCULAR_DEPENDENCY_DETECTION_RECURSION_STOPPER_H +#define CIRCULAR_DEPENDENCY_DETECTION_RECURSION_STOPPER_H + +#include <cstddef> + +namespace ChimeraTK { namespace detail { + + /** A helper class do stop the recursion when scanning for circular dependency networks. + * + * When scanning, each time the whole network has to be detected. This means even if a circular depencency is + * already detected, a module has to scan all of its inputs at least once. So the detection of the + * circle cannot be the point where the recursion is stopped. + * + * The task of this class is to set an indicator the first time a module detects the circle and will then do the + * scan of all inuts, so that following calls can end the recursion because they know the job is done. + * This is done with setRecursionDetected(). + * + * Each input of a module must do a complete scan to determine wheter it is part + * of a circle or not, even if the module itself has other variables in a circle. So the flag must be + * reset at the beginning of each scan. This is done by the static function startNewScan(). + * + * After the call of startNewScan(), recusionDetected() returns false until setRecursionDetected() is called. + * If recursionDetected() after construction before calling startNewScan, an exeption is thrown. + */ + class CircularDependencyDetectionRecursionStopper { + static size_t _globalScanCounter; + size_t _localScanCounter; + + public: + static void startNewScan(); + void setRecursionDetected(); + bool recursionDetected(); + }; + +}} // namespace ChimeraTK::detail + +#endif // CIRCULAR_DEPENDENCY_DETECTION_RECURSION_STOPPER_H diff --git a/include/ControlSystemModule.h b/include/ControlSystemModule.h index d6895aac0386d2d792d98b724d6be45e187b3d29..c3ecf639a3e549cb32894a86fbe5e4a0130fd584 100644 --- a/include/ControlSystemModule.h +++ b/include/ControlSystemModule.h @@ -58,6 +58,8 @@ namespace ChimeraTK { std::list<EntityOwner*> getInputModulesRecursively(std::list<EntityOwner*> startList) override; + size_t getCircularNetworkHash() override; + protected: /** Constructor: the variableNamePrefix will be prepended to all control system variable names (separated by a * slash). Applications should use the [] operator to obtain submodules instead. */ diff --git a/include/DeviceModule.h b/include/DeviceModule.h index ce3ebc65224a979cabe2a2354b6b574f2252cc6a..c5299371855e027b43423c637d275cbec5e81b2c 100644 --- a/include/DeviceModule.h +++ b/include/DeviceModule.h @@ -210,6 +210,8 @@ namespace ChimeraTK { std::list<EntityOwner*> getInputModulesRecursively(std::list<EntityOwner*> startList) override; + size_t getCircularNetworkHash() override; + protected: // populate virtualisedModuleFromCatalog based on the information in the // device's catalogue diff --git a/include/EntityOwner.h b/include/EntityOwner.h index 45af76a8c2ca3c39140ae4720259280f2493ebb1..ae7ff1fa81c51a7d4db80944ed0643412fcb2539 100644 --- a/include/EntityOwner.h +++ b/include/EntityOwner.h @@ -211,6 +211,11 @@ namespace ChimeraTK { /** Use pointer to the module as unique identifier.*/ virtual std::list<EntityOwner*> getInputModulesRecursively(std::list<EntityOwner*> startList) = 0; + /** Get the ID of the circular dependency network (0 if none). This information is only available after + * the Application has finalised all connections. + */ + virtual size_t getCircularNetworkHash() = 0; + protected: /** Add the part of the tree structure matching the given tag to a * VirtualModule. Users normally will use findTag() instead. "tag" is diff --git a/include/InternalModule.h b/include/InternalModule.h index fe41378a80b153586c89efb2ae2d2e5c3ba868fa..cf9468014a41b0352e99e1cad4dd90bcd0124761 100644 --- a/include/InternalModule.h +++ b/include/InternalModule.h @@ -49,6 +49,11 @@ namespace ChimeraTK { "TriggerFanout). This is probably " "caused by incorrect ownership of variables/accessors or VariableGroups."); } + size_t getCircularNetworkHash() override { + throw ChimeraTK::logic_error("getCircularNetworkHash() called on an InternalModule (ThreadedFanout or " + "TriggerFanout). This is probably " + "caused by incorrect ownership of variables/accessors or VariableGroups."); + } }; } /* namespace ChimeraTK */ diff --git a/include/MetaDataPropagatingRegisterDecorator.h b/include/MetaDataPropagatingRegisterDecorator.h index 4ab371e3510e8bca59369741951ff310d577ea32..d95790236d7eb39807328cb5e12848c82abd76e5 100644 --- a/include/MetaDataPropagatingRegisterDecorator.h +++ b/include/MetaDataPropagatingRegisterDecorator.h @@ -7,7 +7,20 @@ namespace ChimeraTK { + // we can only declare the classes here but not use them/include the header to avoid a circular dependency class EntityOwner; + class VariableNetworkNode; + + /** A mix-in helper class so you can set the flags without knowing the user data type. + */ + class MetaDataPropagationFlagProvider { + protected: + bool _isCircularInput{false}; + + // The VariableNetworkNode needs access to _isCircularInput. It cannot be set at construction time because the network is not complete yet + // and isCircularInput is not know at that moment. + friend class VariableNetworkNode; + }; /** * NDRegisterAccessorDecorator which propagates meta data attached to input process variables through the owning @@ -15,7 +28,8 @@ namespace ChimeraTK { * same time it will also propagate the DataValidity flag to/from the owning module. */ template<typename T> - class MetaDataPropagatingRegisterDecorator : public NDRegisterAccessorDecorator<T, T> { + class MetaDataPropagatingRegisterDecorator : public NDRegisterAccessorDecorator<T, T>, + public MetaDataPropagationFlagProvider { public: MetaDataPropagatingRegisterDecorator(const boost::shared_ptr<NDRegisterAccessor<T>>& target, EntityOwner* owner) : NDRegisterAccessorDecorator<T, T>(target), _owner(owner) {} @@ -28,12 +42,13 @@ namespace ChimeraTK { protected: EntityOwner* _owner; - /** value of validity flag from last read operation */ + /** value of validity flag from last read or write operation */ DataValidity lastValidity{DataValidity::ok}; using TransferElement::_dataValidity; using NDRegisterAccessorDecorator<T>::_target; using NDRegisterAccessorDecorator<T>::buffer_2D; + using MetaDataPropagationFlagProvider::_isCircularInput; }; DECLARE_TEMPLATE_FOR_CHIMERATK_USER_TYPES(MetaDataPropagatingRegisterDecorator); diff --git a/include/Module.h b/include/Module.h index bab1c1bc4183b9ef31f5c1e846a629889b6732c4..599594ad5d5e9d80ae9f075f29cc9784906c3399 100644 --- a/include/Module.h +++ b/include/Module.h @@ -164,6 +164,8 @@ namespace ChimeraTK { return _owner->getInputModulesRecursively(startList); } + size_t getCircularNetworkHash() override { return _owner->getCircularNetworkHash(); } + /** Find ApplicationModule owner. If "this" is an ApplicationModule, "this" is returned. If "this" is a * VariableGroup, the tree of owners is followed, until the ApplicationModule is found. If "this" is neither an * ApplicationModule nor a VariableGroup, a ChimeraTK::logic_error is thrown. */ diff --git a/include/VariableNetworkNode.h b/include/VariableNetworkNode.h index fb263b23f85e5d0a1b866abcf3b159092816886c..b3f247cce939dbbbc74de211d1c6cd424edc089a 100644 --- a/include/VariableNetworkNode.h +++ b/include/VariableNetworkNode.h @@ -141,6 +141,9 @@ namespace ChimeraTK { */ std::list<EntityOwner*> scanForCircularDepencency(); + /** Get the unique ID of the circular network. It is 0 if the node is not part of a circular network.*/ + size_t getCircularNetworkHash() const; + /** Getter for the properties */ NodeType getType() const; UpdateMode getMode() const; @@ -275,7 +278,7 @@ namespace ChimeraTK { /** Pointer to the module owning this node */ EntityOwner* owningModule{nullptr}; - /** Hash which idientifies a circular network. I if the node is not part if a circular dependency. */ + /** Hash which idientifies a circular network. 0 if the node is not part if a circular dependency. */ size_t circularNetworkHash{0}; }; @@ -345,6 +348,8 @@ namespace ChimeraTK { void VariableNetworkNode::setAppAccessorImplementation(boost::shared_ptr<NDRegisterAccessor<UserType>> impl) const { auto decorated = boost::make_shared<MetaDataPropagatingRegisterDecorator<UserType>>(impl, getOwningModule()); getAppAccessor<UserType>().replace(decorated); + auto flagProvider = boost::dynamic_pointer_cast<MetaDataPropagationFlagProvider>(decorated); + assert(flagProvider); } } /* namespace ChimeraTK */ diff --git a/src/ApplicationModule.cc b/src/ApplicationModule.cc index 989db2951e4dfe8914bffb9b7a6f1d064179522e..55363ba959ca5c8fadd139a89b90cdaa5e07f101 100644 --- a/src/ApplicationModule.cc +++ b/src/ApplicationModule.cc @@ -135,19 +135,30 @@ namespace ChimeraTK { /*********************************************************************************************************************/ std::list<EntityOwner*> ApplicationModule::getInputModulesRecursively(std::list<EntityOwner*> startList) { + if(_recursionStopper.recursionDetected()) { + return startList; + } + // If this module is already in the list we found a circular dependency. - // Add this module again, so the caller will see also see the circle, and return. + // Remember this for the next time the recursive scan calls this function if(std::count(startList.begin(), startList.end(), this)) { - startList.push_back(this); - return startList; + _recursionStopper.setRecursionDetected(); } - // loop all inputs + // Whether a cirular depencency has been detected or not, we must loop all inputs and add this module to the list so the calling + // code sees the second instance and can also detect the circle. + // The reason why we have to scan all inputs even if a circle is detected is this: + // * A single input starts the scan by adding it's owning module. At this point not all inputs if that module are in the circular network. + // * When a circle is detected, it might only be one of multiple entangled circled. If we would break the recursion and not scan all the + // inputs this is sufficient to identify that the particular input is in a circle. But at this point we have to tell in which network it + // is and have to scan the complete network to calculate the correct hash value. + startList.push_back(this); // first we add this module to the start list. We will call all inputs with it. std::list<EntityOwner*> returnList{ startList}; // prepare the return list. Deltas from the inputs will be added to it. for(auto& accessor : this->getAccessorListRecursive()) { - if(accessor.getDirection().dir != VariableDirection::consuming) continue; // not an input (consuming from network) + // not consumun from network -> not an input, just continue + if(accessor.getDirection().dir != VariableDirection::consuming) continue; // find the feeder in the network auto feeder = accessor.getOwner().getFeedingNode(); @@ -170,4 +181,33 @@ namespace ChimeraTK { /*********************************************************************************************************************/ + size_t ApplicationModule::getCircularNetworkHash() { return _circularNetworkHash; } + + /*********************************************************************************************************************/ + + void ApplicationModule::setCircularNetworkHash(size_t circularNetworkHash) { + if(_circularNetworkHash != 0 && _circularNetworkHash != circularNetworkHash) { + throw ChimeraTK::logic_error( + "Error: setCircularNetworkHash() called with different values for EntityOwner \"" + _name + "\" "); + } + _circularNetworkHash = circularNetworkHash; + } + + /*********************************************************************************************************************/ + DataValidity ApplicationModule::getDataValidity() const { + if(dataFaultCounter == 0) return DataValidity::ok; + if(_circularNetworkHash != 0) { + // In a circular dependency network, internal inputs are ignored. + // If all external inputs (including the ones from this module) are OK, the + // data valitity is set to OK. + if(Application::getInstance().circularNetworkInvalidityCounters[_circularNetworkHash] == 0) { + return DataValidity::ok; + } + } + // not a circular network or invalidity counter not 0 -> keep the faulty flag + return DataValidity::faulty; + } + + /*********************************************************************************************************************/ + } /* namespace ChimeraTK */ diff --git a/src/CircularDependencyDetectionRecursionStopper.cc b/src/CircularDependencyDetectionRecursionStopper.cc new file mode 100644 index 0000000000000000000000000000000000000000..a6df377b7e89bea8cd774500188aa6fe9d097163 --- /dev/null +++ b/src/CircularDependencyDetectionRecursionStopper.cc @@ -0,0 +1,18 @@ +#include "CircularDependencyDetectionRecursionStopper.h" +#include <ChimeraTK/Exception.h> + +namespace ChimeraTK { namespace detail { + + size_t CircularDependencyDetectionRecursionStopper::_globalScanCounter{0}; + + void CircularDependencyDetectionRecursionStopper::startNewScan() { ++_globalScanCounter; } + void CircularDependencyDetectionRecursionStopper::setRecursionDetected() { _localScanCounter = _globalScanCounter; } + bool CircularDependencyDetectionRecursionStopper::recursionDetected() { + if(_globalScanCounter == 0) { + throw ChimeraTK::logic_error( + "CircularDependencyDetectionRecursionStopper::recursionDetected() called without starting a scan."); + } + return _localScanCounter == _globalScanCounter; + } + +}} // namespace ChimeraTK::detail diff --git a/src/ControlSystemModule.cc b/src/ControlSystemModule.cc index 0ca3e62c536159318d0602ae8bbb1bb3bbfcad1d..0b87c54989aac367ac6ed6999ff0bb99af36a955 100644 --- a/src/ControlSystemModule.cc +++ b/src/ControlSystemModule.cc @@ -83,4 +83,11 @@ namespace ChimeraTK { /*********************************************************************************************************************/ + size_t ControlSystemModule::getCircularNetworkHash() { + throw ChimeraTK::logic_error("getCircularNetworkHash() called on the ControlSystemModule. This is probably " + "caused by incorrect ownership of variables/accessors or VariableGroups."); + } + + /*********************************************************************************************************************/ + } // namespace ChimeraTK diff --git a/src/DeviceModule.cc b/src/DeviceModule.cc index 5461dd59d5dbd3b2a564d5c3fa97b6dbfad7c5ff..200bc50578266f16a82515af21c74a9ae1406a60 100644 --- a/src/DeviceModule.cc +++ b/src/DeviceModule.cc @@ -605,4 +605,13 @@ namespace ChimeraTK { /*********************************************************************************************************************/ + size_t DeviceModule::getCircularNetworkHash() { + throw ChimeraTK::logic_error("getCircularNetworkHash() called on a DeviceModule. This is probably " + "caused by incorrect ownership of variables/accessors or VariableGroups."); + } + + /*********************************************************************************************************************/ + + /*********************************************************************************************************************/ + } // namespace ChimeraTK diff --git a/src/EntityOwner.cc b/src/EntityOwner.cc index d4e0cf963385f0b3a2cee5943ca84908b7cd2a10..b7018a74c996138ee1d539f270a856901dedd018 100644 --- a/src/EntityOwner.cc +++ b/src/EntityOwner.cc @@ -292,6 +292,10 @@ namespace ChimeraTK { return nextmodule; } + /*********************************************************************************************************************/ + bool EntityOwner::hasReachedTestableMode() { return testableModeReached; } + /*********************************************************************************************************************/ + } /* namespace ChimeraTK */ diff --git a/src/MetaDataPropagatingRegisterDecorator.cc b/src/MetaDataPropagatingRegisterDecorator.cc index f71f93577c94450d7921631981f094717f5d65b9..f61639a961cd63d68900a7b7d5310325f6f95402 100644 --- a/src/MetaDataPropagatingRegisterDecorator.cc +++ b/src/MetaDataPropagatingRegisterDecorator.cc @@ -1,5 +1,8 @@ #include "MetaDataPropagatingRegisterDecorator.h" #include "EntityOwner.h" +#include "VariableNetworkNode.h" +#include "Application.h" +#include <boost/pointer_cast.hpp> namespace ChimeraTK { @@ -12,12 +15,22 @@ namespace ChimeraTK { _owner->setCurrentVersionNumber(this->getVersionNumber()); } - // Check if the data validity flag changed. If yes, propagate this information to the owning module. + // Check if the data validity flag changed. If yes, propagate this information to the owning module and the application if(_dataValidity != lastValidity) { - if(_dataValidity == DataValidity::faulty) + if(_dataValidity == DataValidity::faulty) { // data validity changes to faulty _owner->incrementDataFaultCounter(); - else + // external inpput in a circular dependency network + if(_owner->getCircularNetworkHash() && !_isCircularInput) { + ++(Application::getInstance().circularNetworkInvalidityCounters[_owner->getCircularNetworkHash()]); + } + } + else { // data validity changed to OK _owner->decrementDataFaultCounter(); + // external inpput in a circular dependency network + if(_owner->getCircularNetworkHash() && !_isCircularInput) { + --(Application::getInstance().circularNetworkInvalidityCounters[_owner->getCircularNetworkHash()]); + } + } lastValidity = _dataValidity; } } @@ -26,6 +39,20 @@ namespace ChimeraTK { void MetaDataPropagatingRegisterDecorator<T>::doPreWrite(TransferType type, VersionNumber versionNumber) { // We cannot use NDRegisterAccessorDecorator<T> here because we need a different implementation of setting the target data validity. // So we have a complete implemetation here. + + if(_owner->getCircularNetworkHash() && _dataValidity != lastValidity) { + // In circular dependency networks an output which actively has DataValidity::faulty set by the user logic is handled + // as if an external input was invalid -> increase or decrease the network's invalidity counter accordingly + if(_dataValidity == DataValidity::faulty) { // data validity changes to faulty + ++(Application::getInstance().circularNetworkInvalidityCounters[_owner->getCircularNetworkHash()]); + } + else { + --(Application::getInstance().circularNetworkInvalidityCounters[_owner->getCircularNetworkHash()]); + } + lastValidity = _dataValidity; + } + + // Now propagate the flag and the data to the target and perform the write if(_dataValidity == DataValidity::faulty) { // the application has manualy set the validity to faulty _target->setDataValidity(DataValidity::faulty); } diff --git a/src/VariableNetworkNode.cc b/src/VariableNetworkNode.cc index 61cba1dcdfe9c4d17c5d14b7f551263d23fb3dfc..c4874fc32b77f0913a961ba9d23d1a87096c7777 100644 --- a/src/VariableNetworkNode.cc +++ b/src/VariableNetworkNode.cc @@ -13,6 +13,8 @@ #include "Visitor.h" #include "VariableGroup.h" #include <boost/container_hash/hash.hpp> +#include "ApplicationModule.h" +#include "CircularDependencyDetectionRecursionStopper.h" namespace ChimeraTK { @@ -381,12 +383,10 @@ namespace ChimeraTK { /*********************************************************************************************************************/ - std::string printModuleType(EntityOwner::ModuleType type) { - if(type == EntityOwner::ModuleType::ApplicationModule) return "ApplicationModule"; - if(type == EntityOwner::ModuleType::VariableGroup) return "VariableGroup"; - return "don't care"; - } std::list<EntityOwner*> VariableNetworkNode::scanForCircularDepencency() { + // We are starting a new scan. Reset the indicator for already found circular dependencies. + detail::CircularDependencyDetectionRecursionStopper::startNewScan(); + // find the feeder of the network auto feeder = getOwner().getFeedingNode(); auto feedingModule = feeder.getOwningModule(); @@ -416,6 +416,20 @@ namespace ChimeraTK { // Remember that we are part of a circle, and of which circle pdata->circularNetworkHash = boost::hash_range(inputModuleList.begin(), inputModuleList.end()); + // we already did the assertion that the owning module is an application module above, so we can static cast here + auto applicationModule = static_cast<ApplicationModule*>(owningModule); + applicationModule->setCircularNetworkHash(pdata->circularNetworkHash); + + // Find the MetaDataPropagatingRegisterDecorator which is involed and set the _isCurularInput flag + auto internalTargetElements = getAppAccessorNoType().getInternalElements(); + // This is a list of all the nested decorators, so we will find the right point to cast + for(auto& elem : internalTargetElements) { + auto flagProvider = boost::dynamic_pointer_cast<MetaDataPropagationFlagProvider>(elem); + if(flagProvider) { + flagProvider->_isCircularInput = true; + } + } + return inputModuleList; } @@ -446,4 +460,8 @@ namespace ChimeraTK { void VariableNetworkNode::setPublicName(const std::string& name) const { pdata->publicName = name; } + /*********************************************************************************************************************/ + + size_t VariableNetworkNode::getCircularNetworkHash() const { return pdata->circularNetworkHash; } + } // namespace ChimeraTK diff --git a/tests/executables_src/testCircularDependencyFaultyFlags.cc b/tests/executables_src/testCircularDependencyFaultyFlags.cc index ebff7f8ccd229abd97f6ae48c61591748b1eed9b..4eadbad09105ba6dcedd269650233ae087afdf92 100644 --- a/tests/executables_src/testCircularDependencyFaultyFlags.cc +++ b/tests/executables_src/testCircularDependencyFaultyFlags.cc @@ -40,6 +40,7 @@ struct TestModuleBase : ctk::ApplicationModule { while(true) { circularOutput1 = static_cast<int>(inputGroup.circularInput1); outputGroup.circularOutput2 = static_cast<int>(circularInput2); + writeAll(); readAll(); } @@ -52,29 +53,67 @@ struct ModuleA : TestModuleBase { ctk::ScalarPushInput<int> a{this, "a", "", ""}; ctk::ScalarPushInput<int> b{this, "b", "", ""}; + ctk::ScalarOutput<int> circleResult{this, "circleResult", "", ""}; void prepare() override { writeAll(); } void mainLoop() override { - auto rag = readAnyGroup(); + // The circular inputs always are both coming as a pair, but we only want to write once. + // Hence we only put one of them into the ReadAnyGroup and always read the second one manually if the first one + // is read by the group. + ctk::ReadAnyGroup rag({a, b, inputGroup.circularInput1}); while(true) { - rag.readAny(); // we con't care which input has been read. Just update all content + auto id = rag.readAny(); - circularOutput1 = static_cast<int>(inputGroup.circularInput1) + a; - outputGroup.circularOutput2 = static_cast<int>(circularInput2) + b; + // A module with circular inputs and readAny must always actively break the circle. Otherwise for each external + // input and n-1 internal inputs and additional data element is inserted into the circle, which will let queues + // run over and re-trigger the circle all the time. + // This is a very typical scenario for circular connections: A module gets some input, triggers a helper module + // which calculates a value that is read back by the first module, and then the first module continues without + // re-triggering the circle. - writeAll(); - } - } -}; + assert((id == a.getId() || id == b.getId()) || id == inputGroup.circularInput1.getId() || + id == circularInput2.getId()); + + if(id == inputGroup.circularInput1.getId()) { + // Read the other circular input as well. They always come in pairs. + circularInput2.read(); + } + + if(id == a.getId() || id == b.getId()) { + circularOutput1 = static_cast<int>(inputGroup.circularInput1) + a; + outputGroup.circularOutput2 = static_cast<int>(circularInput2) + b; + + circularOutput1.write(); + outputGroup.circularOutput2.write(); + } + else { // new data is from the circular inputs + circleResult = static_cast<int>(inputGroup.circularInput1) + circularInput2; + circleResult.write(); + } + + } // while(true) + } // mainLoop +}; // ModuleA /// ModuleC has a trigger together with a readAll.; (it's a trigger for the circle because there is always something at the circular inputs) struct ModuleC : TestModuleBase { using TestModuleBase::TestModuleBase; ctk::ScalarPushInput<int> trigger{this, "trigger", "", ""}; - // no need to override the main loop again. Nothing to do with the data content of the trigger, and readAll() considers it. + // Special loop to guarantee that the internal inputs are read first, so we don't have unread data in the queue and can use the testable mode + void mainLoop() override { + while(true) { + circularOutput1 = static_cast<int>(inputGroup.circularInput1); + outputGroup.circularOutput2 = static_cast<int>(circularInput2); + writeAll(); + //readAll(); + inputGroup.circularInput1.read(); + circularInput2.read(); + trigger.read(); + } + } }; struct TestApplication1 : ctk::Application { @@ -91,12 +130,78 @@ struct TestApplication1 : ctk::Application { ctk::ControlSystemModule cs; }; +template<typename APP_TYPE> +struct CircularAppTestFixcture { + APP_TYPE app; + ctk::TestFacility test; + + ctk::ScalarRegisterAccessor<int> a{test.getScalar<int>("A/a")}; + ctk::ScalarRegisterAccessor<int> b{test.getScalar<int>("A/b")}; + ctk::ScalarRegisterAccessor<int> C_trigger{test.getScalar<int>("C/trigger")}; + ctk::ScalarRegisterAccessor<int> A_out1{test.getScalar<int>("A/circularOutput1")}; + ctk::ScalarRegisterAccessor<int> B_out1{test.getScalar<int>("B/circularOutput1")}; + ctk::ScalarRegisterAccessor<int> C_out1{test.getScalar<int>("C/circularOutput1")}; + ctk::ScalarRegisterAccessor<int> D_out1{test.getScalar<int>("D/circularOutput1")}; + ctk::ScalarRegisterAccessor<int> A_in2{test.getScalar<int>("A/circularInput2")}; + ctk::ScalarRegisterAccessor<int> B_in2{test.getScalar<int>("B/circularInput2")}; + ctk::ScalarRegisterAccessor<int> C_in2{test.getScalar<int>("C/circularInput2")}; + ctk::ScalarRegisterAccessor<int> D_in2{test.getScalar<int>("D/circularInput2")}; + ctk::ScalarRegisterAccessor<int> circleResult{test.getScalar<int>("A/circleResult")}; + + void readAllLatest() { + A_out1.readLatest(); + B_out1.readLatest(); + C_out1.readLatest(); + D_out1.readLatest(); + A_in2.readLatest(); + B_in2.readLatest(); + C_in2.readLatest(); + D_in2.readLatest(); + circleResult.readLatest(); + } + + void checkAllDataValidity(ctk::DataValidity validity) { + BOOST_CHECK(A_out1.dataValidity() == validity); + BOOST_CHECK(B_out1.dataValidity() == validity); + BOOST_CHECK(C_out1.dataValidity() == validity); + BOOST_CHECK(D_out1.dataValidity() == validity); + BOOST_CHECK(A_in2.dataValidity() == validity); + BOOST_CHECK(B_in2.dataValidity() == validity); + BOOST_CHECK(C_in2.dataValidity() == validity); + BOOST_CHECK(D_in2.dataValidity() == validity); + BOOST_CHECK(circleResult.dataValidity() == validity); + } + + CircularAppTestFixcture() { + test.runApplication(); + a.replace(test.getScalar<int>("A/a")); + b.replace(test.getScalar<int>("A/b")); + C_trigger.replace(test.getScalar<int>("C/trigger")); + A_out1.replace(test.getScalar<int>("A/circularOutput1")); + B_out1.replace(test.getScalar<int>("B/circularOutput1")); + C_out1.replace(test.getScalar<int>("C/circularOutput1")); + D_out1.replace(test.getScalar<int>("D/circularOutput1")); + A_in2.replace(test.getScalar<int>("A/circularInput2")); + B_in2.replace(test.getScalar<int>("B/circularInput2")); + C_in2.replace(test.getScalar<int>("C/circularInput2")); + D_in2.replace(test.getScalar<int>("D/circularInput2")); + circleResult.replace(test.getScalar<int>("A/circleResult")); + } +}; + +/** \anchor dataValidity_test_TestCircularInputDetection + * Tests Technical specification: data validity propagation + * * \ref dataValidity_4_1_1 "4.1.1" Inputs which are part of a circular dependency are marked as circular input. + * * \ref dataValidity_4_1_1_1 "4.1.1.1" (partly, DeviceModule and other ApplciationModules not tested) Inputs from CS are external inputs. + * * \ref dataValidity_4_1_2 "4.1.2" All modules which have a circular dependency form a circular network. + */ BOOST_AUTO_TEST_CASE(TestCircularInputDetection) { TestApplication1 app; ctk::TestFacility test; test.runApplication(); - app.dumpConnections(); + //app.dumpConnections(); + //app.dump(); // just test that the circular inputs have been detected correctly BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.A.inputGroup.circularInput1).isCircularInput() == true); @@ -122,3 +227,445 @@ BOOST_AUTO_TEST_CASE(TestCircularInputDetection) { BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.D.circularOutput1).isCircularInput() == false); BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.D.outputGroup.circularOutput2).isCircularInput() == false); } + +/** \anchor dataValidity_test_OneInvalidVariable + * Tests Technical specification: data validity propagation + * * \ref dataValidity_4_1_4 "4.1.4" Propagation of the invalidity flag in a circle. + * * \ref dataValidity_4_1_5 "4.1.5" Breaking the circular dependency. + * + * This test intentionally does set more than one external input to faulty to make it easier to see where problems are coming from. + */ +BOOST_FIXTURE_TEST_CASE(OneInvalidVariable, CircularAppTestFixcture<TestApplication1>) { + a.setDataValidity(ctk::DataValidity::faulty); + a.write(); + C_trigger.write(); + test.stepApplication(); + + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + // getting a valid variable in the same module does not resolve the flag + b.write(); + C_trigger.write(); + test.stepApplication(); + + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + // now resolve the faulty condition + a.setDataValidity(ctk::DataValidity::ok); + a.write(); + test.stepApplication(); + + readAllLatest(); + // we check in the app that the input is still invalid, not in the CS + BOOST_CHECK(app.A.inputGroup.circularInput1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(app.A.circularInput2.dataValidity() == ctk::DataValidity::faulty); + // the circular outputs of A and B are now valid + BOOST_CHECK(A_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(B_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(B_in2.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(C_in2.dataValidity() == ctk::DataValidity::ok); + // the outputs of C, D and the circularResult have not been written yet + BOOST_CHECK(C_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(D_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(A_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(D_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(circleResult.dataValidity() == ctk::DataValidity::faulty); + + // Now trigger C. The whole circle resolves + C_trigger.write(); + test.stepApplication(); + readAllLatest(); + + checkAllDataValidity(ctk::DataValidity::ok); +} + +/** \anchor dataValidity_test_TwoFaultyInOneModule + * Tests Technical specification: data validity propagation + * * \ref dataValidity_4_1_5 "4.1.5" Breaking the circular dependency only when all variables go to ok. + */ +BOOST_FIXTURE_TEST_CASE(TwoFaultyInOneModule, CircularAppTestFixcture<TestApplication1>) { + a.setDataValidity(ctk::DataValidity::faulty); + a.write(); + C_trigger.write(); + test.stepApplication(); + // new in this test: an additional variable comes in while the internal and other external inputs are invalid + b.setDataValidity(ctk::DataValidity::faulty); + b.write(); + C_trigger.write(); + test.stepApplication(); + + // just a cross check + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + a.setDataValidity(ctk::DataValidity::ok); + a.write(); + C_trigger.write(); + test.stepApplication(); + + // everything still faulty as b is faulty + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + b.setDataValidity(ctk::DataValidity::ok); + b.write(); + C_trigger.write(); + test.stepApplication(); + + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::ok); +} + +/** \anchor dataValidity_test_outputManuallyFaulty + * Tests Technical specification: data validity propagation + * * \ref dataValidity_4_1_8 "4.1.8" Programmatically setting an output to faulty behaves like external input faulty. + */ +BOOST_FIXTURE_TEST_CASE(OutputManuallyFaulty, CircularAppTestFixcture<TestApplication1>) { + app.A.circularOutput1.setDataValidity(ctk::DataValidity::faulty); + a.write(); + test.stepApplication(); + + readAllLatest(); + // The data validity flag is not ignored, although only circular inputs are invalid + // B transports the flag. The A.outputGroup.circularOutput2 is still valid because all inputs are valid. + BOOST_CHECK(A_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(B_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(B_in2.dataValidity() == ctk::DataValidity::ok); // this is A.outputGroup.circularOutput2 + BOOST_CHECK(C_in2.dataValidity() == ctk::DataValidity::faulty); + // the outputs of C, D and the circularResult have already been written yet + BOOST_CHECK(C_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(D_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(A_in2.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(D_in2.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(circleResult.dataValidity() == ctk::DataValidity::ok); + + C_trigger.write(); + test.stepApplication(); + + // Now the whole circle is invalid, except for A.outputGroup.circularOutput2 which has not been written again yet. + // (Module A stops the circular propagation because it is using readAny(), which otherwises would lead to more and more + // data packages piling up in the cirle because each external read adds one) + readAllLatest(); + BOOST_CHECK(A_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(B_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(B_in2.dataValidity() == ctk::DataValidity::ok); // this is A.outputGroup.circularOutput2 + BOOST_CHECK(C_in2.dataValidity() == ctk::DataValidity::faulty); + // the outputs of C, D and the circularResult have already been written yet + BOOST_CHECK(C_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(D_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(A_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(D_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(circleResult.dataValidity() == ctk::DataValidity::faulty); + + // If we now complete the circle again, the faulty flag is propagated everywhere + a.write(); + C_trigger.write(); + test.stepApplication(); + + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + // Check that the situation resolved when the data validity of the output is back to ok + app.A.circularOutput1.setDataValidity(ctk::DataValidity::ok); + a.write(); + test.stepApplication(); + + readAllLatest(); + // Module A goes to valid immediately and ignored the invalid circular inputs + BOOST_CHECK(A_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(B_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(B_in2.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(C_in2.dataValidity() == ctk::DataValidity::ok); + // the outputs of C, D and the circularResult have not been written yet + BOOST_CHECK(C_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(D_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(A_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(D_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(circleResult.dataValidity() == ctk::DataValidity::faulty); + + C_trigger.write(); + test.stepApplication(); + + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::ok); +} + +/** \anchor dataValidity_test_TwoFaultyInTwoModules + * Tests Technical specification: data validity propagation + * * \ref dataValidity_4_2_3 "4.2.3" Modules do no go to OK if all its external inputs are OK + * if other modules in the circular network have external inputs which are faulty. + */ +BOOST_FIXTURE_TEST_CASE(TwoFaultyInTwoModules, CircularAppTestFixcture<TestApplication1>) { + a.setDataValidity(ctk::DataValidity::faulty); + a.write(); + C_trigger.write(); + test.stepApplication(); + // new in this test: the trigger in C bring an additional invalidity flag. + a.write(); + C_trigger.setDataValidity(ctk::DataValidity::faulty); + C_trigger.write(); + test.stepApplication(); + + // just a cross check + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + a.setDataValidity(ctk::DataValidity::ok); + a.write(); + C_trigger.write(); + test.stepApplication(); + + // everything still faulty as b is faulty + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::faulty); + + a.write(); + C_trigger.setDataValidity(ctk::DataValidity::ok); + C_trigger.write(); + test.stepApplication(); + + readAllLatest(); + // the first half of the circle is not OK yet because no external triggers have arrived at A since + // the faultly condition was resolved + BOOST_CHECK(A_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(B_out1.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(B_in2.dataValidity() == ctk::DataValidity::faulty); + BOOST_CHECK(C_in2.dataValidity() == ctk::DataValidity::faulty); + // the outputs of C, D and the circularResult have already been written yet + BOOST_CHECK(C_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(D_out1.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(A_in2.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(D_in2.dataValidity() == ctk::DataValidity::ok); + BOOST_CHECK(circleResult.dataValidity() == ctk::DataValidity::ok); + + // writing a resolves the remainging variables + a.write(); + test.stepApplication(); + readAllLatest(); + checkAllDataValidity(ctk::DataValidity::ok); +} + +// A more complicated network with three entangled circles and one separate circle. +// AA-->BB-->CC-->DD-->AA /->HH +// ^ | | ^ GG<-/ +// |-EE<-| |->FF-| +// +// The important part of this test is to check that the whole network AA,..,FF is always detected for each input, +// even if the scan is only for a variable that starts the scan in only in a local circle (like AA/fromEE). +// In addition it tests that not everything is mixed into a single circular network (GG,HH is detected as separate circular network). + +// Don't try to pass any data through the network. It will be stuck because there are no real main loops. Only the initial value is passed (write exaclty once, then never read). +// It's just used to test the static circular network detection. + +struct TestModuleBase2 : ctk::ApplicationModule { + using ApplicationModule::ApplicationModule; + + // available in all modules + ctk::ScalarPushInput<int> fromCS{this, "fromCS", "", ""}; + + // default main loop which provides initial values, but does not read or write anything else + void mainLoop() override { writeAll(); } +}; + +struct AA : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromEE{this, "fromEE", "", ""}; + ctk::ScalarPushInput<int> fromDD{this, "fromDD", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromAA{this, "fromAA", "", ""}; + } outputGroup{this, "BB", "", ctk::HierarchyModifier::oneLevelUp}; + + void prepare() override { writeAll(); } // break circular waiting for initial values + void mainLoop() override {} +}; + +struct BB : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromAA{this, "fromAA", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromBB{this, "fromBB", "", ""}; + } outputGroup{this, "CC", "", ctk::HierarchyModifier::oneLevelUp}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromBB{this, "fromBB", "", ""}; + } outputGroup2{this, "EE", "", ctk::HierarchyModifier::oneLevelUp}; +}; + +struct EE : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromBB{this, "fromBB", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromEE{this, "fromEE", "", ""}; + } outputGroup{this, "AA", "", ctk::HierarchyModifier::oneLevelUp}; +}; + +struct CC : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromBB{this, "fromBB", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromCC{this, "fromCC", "", ""}; + } outputGroup{this, "DD", "", ctk::HierarchyModifier::oneLevelUp}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromCC{this, "fromCC", "", ""}; + } outputGroup2{this, "FF", "", ctk::HierarchyModifier::oneLevelUp}; +}; + +struct DD : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromCC{this, "fromCC", "", ""}; + ctk::ScalarPushInput<int> fromFF{this, "fromFF", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromDD{this, "fromDD", "", ""}; + } outputGroup{this, "AA", "", ctk::HierarchyModifier::oneLevelUp}; +}; + +struct FF : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromCC{this, "fromCC", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromFF{this, "fromFF", "", ""}; + } outputGroup{this, "DD", "", ctk::HierarchyModifier::oneLevelUp}; +}; + +struct GG : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromHH{this, "fromHH", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromGG{this, "fromGG", "", ""}; + } outputGroup{this, "HH", "", ctk::HierarchyModifier::oneLevelUp}; + + void prepare() override { writeAll(); } // break circular waiting for initial values + void mainLoop() override {} +}; + +struct HH : TestModuleBase2 { + using TestModuleBase2::TestModuleBase2; + + ctk::ScalarPushInput<int> fromGG{this, "fromGG", "", ""}; + + struct /*OutputGroup*/ : public ctk::VariableGroup { + using ctk::VariableGroup::VariableGroup; + ctk::ScalarOutput<int> fromHH{this, "fromHH", "", ""}; + } outputGroup{this, "GG", "", ctk::HierarchyModifier::oneLevelUp}; +}; + +struct TestApplication2 : ctk::Application { + TestApplication2() : Application("connectionTestSuite") {} + ~TestApplication2() { shutdown(); } + + void defineConnections() { findTag(".*").connectTo(cs); } + + AA aa{this, "AA", ""}; + BB bb{this, "BB", ""}; + CC cc{this, "CC", ""}; + DD dd{this, "DD", ""}; + EE ee{this, "EE", ""}; + FF ff{this, "FF", ""}; + GG gg{this, "GG", ""}; + HH hh{this, "HH", ""}; + + ctk::ControlSystemModule cs; + + public: + std::map<size_t, std::list<EntityOwner*>> getCircularDependencyNetworks() { + return Application::circularDependencyNetworks; + } +}; + +/** \anchor dataValidity_test_TestCircularInputDetection2 + * Tests Technical specification: data validity propagation + * * \ref dataValidity_4_1_2_1 "4.1.2.1" Entangled circles belonhg to the same circular network. + * * \ref dataValidity_4_1_2_2 "4.1.2.2" There can be multiple disconnected circular networks. + * * \ref dataValidity_4_3_2 "4.3.2" Each module and each circular input knows its circular network. + */ +BOOST_AUTO_TEST_CASE(TestCircularInputDetection2) { + TestApplication2 app; + ctk::TestFacility test; + + test.runApplication(); + //app.dumpConnections(); + + // Check that all inputs have been identified correctly + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.aa.fromEE).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.aa.fromDD).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.aa.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.bb.fromAA).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.bb.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.cc.fromBB).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.cc.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.cc.fromBB).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.cc.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.dd.fromCC).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.dd.fromFF).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.dd.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.ee.fromBB).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.ee.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.ff.fromCC).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.ff.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.gg.fromHH).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.gg.fromCS).isCircularInput() == false); + + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.hh.fromGG).isCircularInput() == true); + BOOST_CHECK(static_cast<ctk::VariableNetworkNode>(app.hh.fromCS).isCircularInput() == false); + + // Check that the networks have been identified correctly + auto circularNetworks = app.getCircularDependencyNetworks(); + BOOST_CHECK_EQUAL(circularNetworks.size(), 2); + for(auto& networkIter : app.getCircularDependencyNetworks()) { + auto& id = networkIter.first; + auto& network = networkIter.second; + // networks have the correct size + if(network.size() == 6) { + std::list<ctk::EntityOwner*> modules = {&app.aa, &app.bb, &app.cc, &app.dd, &app.ee, &app.ff}; + for(auto module : modules) { + // all modules are in the network + BOOST_CHECK(std::count(network.begin(), network.end(), module) == 1); + // each module has the correct network associated + BOOST_CHECK_EQUAL(module->getCircularNetworkHash(), id); + } + } + else if(network.size() == 2) { + std::list<ctk::EntityOwner*> modules = {&app.gg, &app.hh}; + for(auto module : modules) { + BOOST_CHECK(std::count(network.begin(), network.end(), module) == 1); + BOOST_CHECK_EQUAL(module->getCircularNetworkHash(), id); + } + } + else { + BOOST_CHECK_MESSAGE(false, "Network with wrong number of modules detected: " + std::to_string(network.size())); + } + } +}