From fd1ed700548e77cc5899d6c304cdadc0cb055a7d Mon Sep 17 00:00:00 2001 From: kevin delmas Date: Fri, 16 Jun 2023 10:51:58 +0200 Subject: [PATCH] Initial commit --- AUTHORS.txt | 13 + LICENCE_PML_Analyzer.txt | 24 + Makefile | 78 ++ README.md | 176 +++++ build.sbt | 115 +++ doc/_assets/images/phylog_logo.gif | 0 .../images/simpleKeystone/interference.PNG | 0 .../images/simpleKeystone/platform.PNG | 0 .../images/simpleKeystone/temporal_slices.PNG | 0 .../images/simpleKeystone/transactions.PNG | 0 .../simpleKeystone/transactions_footprint.PNG | 0 .../images/simpleT1042/interference.PNG | 0 doc/_assets/images/simpleT1042/platform.PNG | 0 .../images/simpleT1042/transactions.PNG | 0 doc/_assets/text/rootContent.txt | 0 doc/_docs/example/simpleKeystone/export.md | 0 doc/_docs/example/simpleKeystone/index.md | 0 doc/_docs/example/simpleKeystone/library.md | 0 doc/_docs/example/simpleKeystone/platform.md | 0 doc/_docs/example/simpleKeystone/routing.md | 0 doc/_docs/example/simpleKeystone/slicing.md | 0 doc/_docs/example/simpleKeystone/software.md | 0 .../example/simpleKeystone/specification.md | 0 doc/_docs/example/simpleT1042/index.md | 0 doc/_docs/index.md | 0 .../gettingStarted/external_dependency.md | 0 .../gettingStarted/getting_started_SBT.md | 0 .../gettingStarted/getting_started_docker.md | 0 doc/_docs/reference/gettingStarted/index.md | 0 .../reference/usePML/editing_PML_model.md | 0 doc/_docs/reference/usePML/index.md | 0 doc/_docs/reference/usePML/pml_examples.md | 0 doc/_layouts/base.html | 0 doc/_layouts/doc-page.html | 0 doc/_layouts/index.html | 0 doc/_layouts/main.html | 0 doc/_layouts/static-site-main.html | 0 doc/sidebar.yml | 0 lesser.txt | 502 +++++++++++++ minimalBuildSBT.txt | 10 + project/build.properties | 1 + project/plugins.sbt | 2 + .../pml/examples/simpleKeystone/SimpleDoc.md | 149 ++++ .../examples/simpleKeystone/interference.PNG | Bin 0 -> 71823 bytes .../pml/examples/simpleKeystone/platform.PNG | Bin 0 -> 47942 bytes .../simpleKeystone/temporal_slices.PNG | Bin 0 -> 12889 bytes .../examples/simpleKeystone/transactions.PNG | Bin 0 -> 101345 bytes .../simpleKeystone/transactions_footprint.PNG | Bin 0 -> 80327 bytes .../pml/examples/simpleT1042/SimpleDoc.md | 104 +++ .../pml/examples/simpleT1042/interference.PNG | Bin 0 -> 37286 bytes .../pml/examples/simpleT1042/platform.PNG | Bin 0 -> 23007 bytes .../pml/examples/simpleT1042/transactions.PNG | Bin 0 -> 39474 bytes src/main/resources/icons/generic/andBlue.gif | Bin 0 -> 599 bytes src/main/resources/icons/generic/andGreen.gif | Bin 0 -> 394 bytes src/main/resources/icons/generic/andRed.gif | Bin 0 -> 391 bytes .../resources/icons/generic/ctClosedBlue.gif | Bin 0 -> 747 bytes .../resources/icons/generic/ctClosedGreen.gif | Bin 0 -> 735 bytes .../icons/generic/ctClosedPurple.gif | Bin 0 -> 749 bytes .../resources/icons/generic/ctClosedRed.gif | Bin 0 -> 735 bytes .../resources/icons/generic/equalBlue.gif | Bin 0 -> 242 bytes .../resources/icons/generic/equalGreen.gif | Bin 0 -> 250 bytes src/main/resources/icons/generic/equalRed.gif | Bin 0 -> 249 bytes .../icons/generic/function_blue_circle.gif | Bin 0 -> 286 bytes .../icons/generic/function_green_circle.gif | Bin 0 -> 284 bytes .../icons/generic/function_purple_circle.gif | Bin 0 -> 273 bytes .../icons/generic/function_red_circle.gif | Bin 0 -> 274 bytes .../icons/generic/function_red_cross.gif | Bin 0 -> 427 bytes .../resources/icons/generic/obsErrBlue.gif | Bin 0 -> 392 bytes .../resources/icons/generic/obsErrGreen.gif | Bin 0 -> 393 bytes .../resources/icons/generic/obsErrRed.gif | Bin 0 -> 391 bytes src/main/resources/icons/generic/orBlue.gif | Bin 0 -> 378 bytes src/main/resources/icons/generic/orGreen.gif | Bin 0 -> 377 bytes src/main/resources/icons/generic/orRed.gif | Bin 0 -> 376 bytes src/main/resources/icons/generic/preBlue.gif | Bin 0 -> 445 bytes src/main/resources/icons/generic/preGreen.gif | Bin 0 -> 438 bytes .../resources/icons/generic/prePurple.gif | Bin 0 -> 438 bytes src/main/resources/icons/generic/preRed.gif | Bin 0 -> 437 bytes .../resources/icons/generic/select1Green.gif | Bin 0 -> 1136 bytes .../icons/generic/select1LatchRed.gif | Bin 0 -> 746 bytes .../resources/icons/generic/select1Purple.gif | Bin 0 -> 722 bytes .../resources/icons/generic/select1Red.gif | Bin 0 -> 730 bytes .../resources/icons/generic/select2Green.gif | Bin 0 -> 702 bytes .../resources/icons/generic/select2Purple.gif | Bin 0 -> 701 bytes .../resources/icons/generic/select2Red.gif | Bin 0 -> 702 bytes .../resources/icons/generic/selectBlue.gif | Bin 0 -> 724 bytes .../icons/generic/source_blue_circle.gif | Bin 0 -> 416 bytes .../icons/generic/source_green_circle.gif | Bin 0 -> 413 bytes .../icons/generic/source_red_cross.gif | Bin 0 -> 434 bytes .../icons/phylog/phylogBlockBlue.gif | Bin 0 -> 113 bytes .../icons/phylog/phylogBlockGreen.gif | Bin 0 -> 113 bytes .../icons/phylog/phylogBlockGrey.gif | Bin 0 -> 113 bytes .../resources/icons/phylog/phylogBlockRed.gif | Bin 0 -> 113 bytes src/main/scala/pml/examples/package.scala | 6 + .../simpleKeystone/SimpleKeystoneExport.scala | 98 +++ .../SimpleKeystoneLibraryConfiguration.scala | 17 + ...mpleKeystoneLibraryConfigurationFull.scala | 17 + ...mpleKeystoneLibraryConfigurationNoL1.scala | 16 + ...eystoneLibraryConfigurationPlanApp21.scala | 13 + ...eystoneLibraryConfigurationPlanApp22.scala | 15 + .../SimpleKeystonePlatform.scala | 208 ++++++ .../SimpleKeystoneTransactionLibrary.scala | 82 +++ .../SimpleRoutingConfiguration.scala | 16 + .../SimpleSoftwareAllocation.scala | 126 ++++ .../pml/examples/simpleKeystone/package.scala | 12 + .../SimpleRoutingConfiguration.scala | 12 + .../SimpleSoftwareAllocation.scala | 85 +++ .../simpleT1042/SimpleT1042Export.scala | 66 ++ .../SimpleT1042LibraryConfiguration.scala | 10 + .../SimpleT1042LibraryConfigurationFull.scala | 14 + .../SimpleT1042LibraryConfigurationNoL1.scala | 13 + ...leT1042LibraryConfigurationPlanApp21.scala | 10 + ...leT1042LibraryConfigurationPlanApp22.scala | 12 + .../simpleT1042/SimpleT1042Platform.scala | 102 +++ .../SimpleT1042TransactionLibrary.scala | 35 + .../pml/examples/simpleT1042/package.scala | 6 + .../scala/pml/exporters/FileManager.scala | 99 +++ .../pml/exporters/RelationExporter.scala | 260 +++++++ .../scala/pml/exporters/UMLExporter.scala | 660 +++++++++++++++++ src/main/scala/pml/exporters/package.scala | 14 + src/main/scala/pml/model/PMLNode.scala | 47 ++ src/main/scala/pml/model/PMLNodeBuilder.scala | 39 + .../configuration/TransactionLibrary.scala | 464 ++++++++++++ .../pml/model/configuration/package.scala | 13 + .../hardware/BaseHardwareNodeBuilder.scala | 118 ++++ .../scala/pml/model/hardware/Composite.scala | 113 +++ .../scala/pml/model/hardware/Hardware.scala | 31 + .../scala/pml/model/hardware/Initiator.scala | 41 ++ .../scala/pml/model/hardware/Platform.scala | 208 ++++++ .../model/hardware/SimpleTransporter.scala | 41 ++ .../scala/pml/model/hardware/Target.scala | 41 ++ .../pml/model/hardware/Transporter.scala | 31 + .../pml/model/hardware/Virtualizer.scala | 41 ++ .../scala/pml/model/hardware/package.scala | 9 + src/main/scala/pml/model/package.scala | 10 + .../AntiReflexiveSymmetricEndomorphism.scala | 17 + .../model/relations/AuthorizeRelation.scala | 28 + .../pml/model/relations/Endomorphism.scala | 57 ++ .../pml/model/relations/LinkRelation.scala | 35 + .../pml/model/relations/ProvideRelation.scala | 29 + .../ReflexiveSymmetricEndomorphism.scala | 18 + .../scala/pml/model/relations/Relation.scala | 136 ++++ .../pml/model/relations/RoutingRelation.scala | 28 + .../pml/model/relations/UseRelation.scala | 48 ++ .../scala/pml/model/relations/package.scala | 8 + .../pml/model/service/ArtificialService.scala | 41 ++ .../model/service/BaseServiceBuilder.scala | 55 ++ src/main/scala/pml/model/service/Load.scala | 41 ++ .../scala/pml/model/service/Service.scala | 30 + src/main/scala/pml/model/service/Store.scala | 41 ++ .../scala/pml/model/service/package.scala | 9 + .../pml/model/software/Application.scala | 43 ++ .../software/BaseSoftwareNodeBuilder.scala | 54 ++ src/main/scala/pml/model/software/Data.scala | 60 ++ .../scala/pml/model/software/package.scala | 9 + src/main/scala/pml/model/utils/Message.scala | 112 +++ src/main/scala/pml/model/utils/Owner.scala | 8 + src/main/scala/pml/model/utils/package.scala | 6 + .../scala/pml/operators/AsTransaction.scala | 41 ++ src/main/scala/pml/operators/Deactivate.scala | 95 +++ src/main/scala/pml/operators/Link.scala | 163 +++++ src/main/scala/pml/operators/Linked.scala | 98 +++ src/main/scala/pml/operators/Merge.scala | 167 +++++ src/main/scala/pml/operators/Provided.scala | 244 +++++++ src/main/scala/pml/operators/Restrict.scala | 362 ++++++++++ src/main/scala/pml/operators/Route.scala | 181 +++++ src/main/scala/pml/operators/Use.scala | 178 +++++ src/main/scala/pml/operators/Used.scala | 317 +++++++++ src/main/scala/pml/operators/package.scala | 28 + src/main/scala/pml/package.scala | 86 +++ .../dependability/executor/Scheduler.scala | 35 + .../dependability/executor/Simulator.scala | 73 ++ .../dependability/executor/Synchronize.scala | 79 +++ .../exporters/AutomatonCeciliaExporter.scala | 120 ++++ .../BasicOperationCeciliaExporter.scala | 313 ++++++++ .../exporters/CeciliaExporter.scala | 57 ++ .../exporters/ExprCeciliaExporter.scala | 111 +++ .../dependability/exporters/Folder.scala | 140 ++++ .../exporters/GenericImage.scala | 55 ++ .../views/dependability/exporters/Model.scala | 558 +++++++++++++++ .../exporters/PhylogFolder.scala | 42 ++ .../exporters/PlatformCeciliaExporter.scala | 210 ++++++ .../exporters/SoftwareCeciliaExporter.scala | 106 +++ .../exporters/SystemCeciliaExporter.scala | 62 ++ .../exporters/TargetCeciliaExporter.scala | 142 ++++ .../TransporterCeciliaExporter.scala | 281 ++++++++ .../exporters/TypeCeciliaExporter.scala | 35 + .../dependability/exporters/package.scala | 12 + .../views/dependability/model/Component.scala | 24 + .../views/dependability/model/Copy.scala | 20 + .../dependability/model/CustomTypes.scala | 40 ++ .../views/dependability/model/Direction.scala | 37 + .../dependability/model/EnumFailureMode.scala | 44 ++ .../views/dependability/model/Event.scala | 53 ++ .../views/dependability/model/Fire.scala | 35 + .../scala/views/dependability/model/Id.scala | 83 +++ .../dependability/model/ModeAutomaton.scala | 129 ++++ .../views/dependability/model/Software.scala | 90 +++ .../views/dependability/model/System.scala | 48 ++ .../views/dependability/model/Target.scala | 72 ++ .../dependability/model/Transition.scala | 27 + .../dependability/model/Transporter.scala | 161 +++++ .../views/dependability/model/Variable.scala | 217 ++++++ .../operators/IsCriticityOrdering.scala | 34 + .../dependability/operators/IsFinite.scala | 50 ++ .../dependability/operators/IsMergeable.scala | 47 ++ .../operators/IsOptionLike.scala | 40 ++ .../operators/IsShadowOrdering.scala | 36 + .../dependability/operators/package.scala | 6 + .../views/interference/examples/package.scala | 7 + ...eTableBasedInterferenceSpecification.scala | 42 ++ ...SimpleKeystoneInterferenceGeneration.scala | 43 ++ ...lTableBasedInterferenceSpecification.scala | 51 ++ .../examples/simpleKeystone/package.scala | 10 + .../SimpleT1042InterferenceGeneration.scala | 38 + ...2TableBasedInterferenceSpecification.scala | 51 ++ .../interference/exporters/IDPExporter.scala | 257 +++++++ .../exporters/InterferenceGraphExporter.scala | 59 ++ .../interference/exporters/package.scala | 13 + .../model/formalisation/BDDFactory.scala | 600 ++++++++++++++++ .../model/formalisation/ProblemElement.scala | 237 +++++++ .../model/formalisation/package.scala | 8 + .../views/interference/model/package.scala | 10 + .../model/relations/EquivalenceRelation.scala | 18 + .../model/relations/ExclusiveRelation.scala | 33 + .../model/relations/InterfereRelation.scala | 32 + .../relations/NotInterfereRelation.scala | 31 + .../model/relations/TransparentSet.scala | 20 + .../model/relations/package.scala | 3 + ...eTableBasedInterferenceSpecification.scala | 48 ++ .../InterferenceSpecification.scala | 373 ++++++++++ ...lTableBasedInterferenceSpecification.scala | 7 + .../TableBasedInterferenceSpecification.scala | 150 ++++ .../model/specification/package.scala | 15 + .../interference/operators/Analyse.scala | 667 ++++++++++++++++++ .../interference/operators/Equivalent.scala | 21 + .../interference/operators/Exclusive.scala | 24 + .../interference/operators/Interfere.scala | 74 ++ .../interference/operators/PostProcess.scala | 345 +++++++++ .../interference/operators/Transform.scala | 76 ++ .../interference/operators/Transparent.scala | 41 ++ .../interference/operators/package.scala | 23 + .../scala/views/interference/package.scala | 38 + src/main/scala/views/package.scala | 7 + .../patterns/examples/PhylogPatterns.scala | 224 ++++++ .../patterns/exporters/LatexCodePrinter.scala | 119 ++++ .../exporters/LatexDiagramPrinter.scala | 123 ++++ .../patterns/exporters/allPrinters.scala | 20 + .../views/patterns/model/PatternAST.scala | 128 ++++ tikzStyle.tex | 39 + 249 files changed, 15167 insertions(+) create mode 100644 AUTHORS.txt create mode 100644 LICENCE_PML_Analyzer.txt create mode 100644 Makefile create mode 100644 README.md create mode 100644 build.sbt create mode 100644 doc/_assets/images/phylog_logo.gif create mode 100644 doc/_assets/images/simpleKeystone/interference.PNG create mode 100644 doc/_assets/images/simpleKeystone/platform.PNG create mode 100644 doc/_assets/images/simpleKeystone/temporal_slices.PNG create mode 100644 doc/_assets/images/simpleKeystone/transactions.PNG create mode 100644 doc/_assets/images/simpleKeystone/transactions_footprint.PNG create mode 100644 doc/_assets/images/simpleT1042/interference.PNG create mode 100644 doc/_assets/images/simpleT1042/platform.PNG create mode 100644 doc/_assets/images/simpleT1042/transactions.PNG create mode 100644 doc/_assets/text/rootContent.txt create mode 100644 doc/_docs/example/simpleKeystone/export.md create mode 100644 doc/_docs/example/simpleKeystone/index.md create mode 100644 doc/_docs/example/simpleKeystone/library.md create mode 100644 doc/_docs/example/simpleKeystone/platform.md create mode 100644 doc/_docs/example/simpleKeystone/routing.md create mode 100644 doc/_docs/example/simpleKeystone/slicing.md create mode 100644 doc/_docs/example/simpleKeystone/software.md create mode 100644 doc/_docs/example/simpleKeystone/specification.md create mode 100644 doc/_docs/example/simpleT1042/index.md create mode 100644 doc/_docs/index.md create mode 100644 doc/_docs/reference/gettingStarted/external_dependency.md create mode 100644 doc/_docs/reference/gettingStarted/getting_started_SBT.md create mode 100644 doc/_docs/reference/gettingStarted/getting_started_docker.md create mode 100644 doc/_docs/reference/gettingStarted/index.md create mode 100644 doc/_docs/reference/usePML/editing_PML_model.md create mode 100644 doc/_docs/reference/usePML/index.md create mode 100644 doc/_docs/reference/usePML/pml_examples.md create mode 100644 doc/_layouts/base.html create mode 100644 doc/_layouts/doc-page.html create mode 100644 doc/_layouts/index.html create mode 100644 doc/_layouts/main.html create mode 100644 doc/_layouts/static-site-main.html create mode 100644 doc/sidebar.yml create mode 100644 lesser.txt create mode 100644 minimalBuildSBT.txt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 src/main/doc-resources/pml/examples/simpleKeystone/SimpleDoc.md create mode 100644 src/main/doc-resources/pml/examples/simpleKeystone/interference.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleKeystone/platform.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleKeystone/temporal_slices.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleKeystone/transactions.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleKeystone/transactions_footprint.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleT1042/SimpleDoc.md create mode 100644 src/main/doc-resources/pml/examples/simpleT1042/interference.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleT1042/platform.PNG create mode 100644 src/main/doc-resources/pml/examples/simpleT1042/transactions.PNG create mode 100644 src/main/resources/icons/generic/andBlue.gif create mode 100644 src/main/resources/icons/generic/andGreen.gif create mode 100644 src/main/resources/icons/generic/andRed.gif create mode 100644 src/main/resources/icons/generic/ctClosedBlue.gif create mode 100644 src/main/resources/icons/generic/ctClosedGreen.gif create mode 100644 src/main/resources/icons/generic/ctClosedPurple.gif create mode 100644 src/main/resources/icons/generic/ctClosedRed.gif create mode 100644 src/main/resources/icons/generic/equalBlue.gif create mode 100644 src/main/resources/icons/generic/equalGreen.gif create mode 100644 src/main/resources/icons/generic/equalRed.gif create mode 100644 src/main/resources/icons/generic/function_blue_circle.gif create mode 100644 src/main/resources/icons/generic/function_green_circle.gif create mode 100644 src/main/resources/icons/generic/function_purple_circle.gif create mode 100644 src/main/resources/icons/generic/function_red_circle.gif create mode 100644 src/main/resources/icons/generic/function_red_cross.gif create mode 100644 src/main/resources/icons/generic/obsErrBlue.gif create mode 100644 src/main/resources/icons/generic/obsErrGreen.gif create mode 100644 src/main/resources/icons/generic/obsErrRed.gif create mode 100644 src/main/resources/icons/generic/orBlue.gif create mode 100644 src/main/resources/icons/generic/orGreen.gif create mode 100644 src/main/resources/icons/generic/orRed.gif create mode 100644 src/main/resources/icons/generic/preBlue.gif create mode 100644 src/main/resources/icons/generic/preGreen.gif create mode 100644 src/main/resources/icons/generic/prePurple.gif create mode 100644 src/main/resources/icons/generic/preRed.gif create mode 100644 src/main/resources/icons/generic/select1Green.gif create mode 100644 src/main/resources/icons/generic/select1LatchRed.gif create mode 100644 src/main/resources/icons/generic/select1Purple.gif create mode 100644 src/main/resources/icons/generic/select1Red.gif create mode 100644 src/main/resources/icons/generic/select2Green.gif create mode 100644 src/main/resources/icons/generic/select2Purple.gif create mode 100644 src/main/resources/icons/generic/select2Red.gif create mode 100644 src/main/resources/icons/generic/selectBlue.gif create mode 100644 src/main/resources/icons/generic/source_blue_circle.gif create mode 100644 src/main/resources/icons/generic/source_green_circle.gif create mode 100644 src/main/resources/icons/generic/source_red_cross.gif create mode 100644 src/main/resources/icons/phylog/phylogBlockBlue.gif create mode 100644 src/main/resources/icons/phylog/phylogBlockGreen.gif create mode 100644 src/main/resources/icons/phylog/phylogBlockGrey.gif create mode 100644 src/main/resources/icons/phylog/phylogBlockRed.gif create mode 100644 src/main/scala/pml/examples/package.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneExport.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfiguration.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationFull.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationNoL1.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp21.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp22.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystonePlatform.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneTransactionLibrary.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleRoutingConfiguration.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/SimpleSoftwareAllocation.scala create mode 100644 src/main/scala/pml/examples/simpleKeystone/package.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleRoutingConfiguration.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleSoftwareAllocation.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042Export.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfiguration.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationFull.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationNoL1.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp21.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp22.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042Platform.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/SimpleT1042TransactionLibrary.scala create mode 100644 src/main/scala/pml/examples/simpleT1042/package.scala create mode 100644 src/main/scala/pml/exporters/FileManager.scala create mode 100644 src/main/scala/pml/exporters/RelationExporter.scala create mode 100644 src/main/scala/pml/exporters/UMLExporter.scala create mode 100644 src/main/scala/pml/exporters/package.scala create mode 100644 src/main/scala/pml/model/PMLNode.scala create mode 100644 src/main/scala/pml/model/PMLNodeBuilder.scala create mode 100644 src/main/scala/pml/model/configuration/TransactionLibrary.scala create mode 100644 src/main/scala/pml/model/configuration/package.scala create mode 100644 src/main/scala/pml/model/hardware/BaseHardwareNodeBuilder.scala create mode 100644 src/main/scala/pml/model/hardware/Composite.scala create mode 100644 src/main/scala/pml/model/hardware/Hardware.scala create mode 100644 src/main/scala/pml/model/hardware/Initiator.scala create mode 100644 src/main/scala/pml/model/hardware/Platform.scala create mode 100644 src/main/scala/pml/model/hardware/SimpleTransporter.scala create mode 100644 src/main/scala/pml/model/hardware/Target.scala create mode 100644 src/main/scala/pml/model/hardware/Transporter.scala create mode 100644 src/main/scala/pml/model/hardware/Virtualizer.scala create mode 100644 src/main/scala/pml/model/hardware/package.scala create mode 100644 src/main/scala/pml/model/package.scala create mode 100644 src/main/scala/pml/model/relations/AntiReflexiveSymmetricEndomorphism.scala create mode 100644 src/main/scala/pml/model/relations/AuthorizeRelation.scala create mode 100644 src/main/scala/pml/model/relations/Endomorphism.scala create mode 100644 src/main/scala/pml/model/relations/LinkRelation.scala create mode 100644 src/main/scala/pml/model/relations/ProvideRelation.scala create mode 100644 src/main/scala/pml/model/relations/ReflexiveSymmetricEndomorphism.scala create mode 100644 src/main/scala/pml/model/relations/Relation.scala create mode 100644 src/main/scala/pml/model/relations/RoutingRelation.scala create mode 100644 src/main/scala/pml/model/relations/UseRelation.scala create mode 100644 src/main/scala/pml/model/relations/package.scala create mode 100644 src/main/scala/pml/model/service/ArtificialService.scala create mode 100644 src/main/scala/pml/model/service/BaseServiceBuilder.scala create mode 100644 src/main/scala/pml/model/service/Load.scala create mode 100644 src/main/scala/pml/model/service/Service.scala create mode 100644 src/main/scala/pml/model/service/Store.scala create mode 100644 src/main/scala/pml/model/service/package.scala create mode 100644 src/main/scala/pml/model/software/Application.scala create mode 100644 src/main/scala/pml/model/software/BaseSoftwareNodeBuilder.scala create mode 100644 src/main/scala/pml/model/software/Data.scala create mode 100644 src/main/scala/pml/model/software/package.scala create mode 100644 src/main/scala/pml/model/utils/Message.scala create mode 100644 src/main/scala/pml/model/utils/Owner.scala create mode 100644 src/main/scala/pml/model/utils/package.scala create mode 100644 src/main/scala/pml/operators/AsTransaction.scala create mode 100644 src/main/scala/pml/operators/Deactivate.scala create mode 100644 src/main/scala/pml/operators/Link.scala create mode 100644 src/main/scala/pml/operators/Linked.scala create mode 100644 src/main/scala/pml/operators/Merge.scala create mode 100644 src/main/scala/pml/operators/Provided.scala create mode 100644 src/main/scala/pml/operators/Restrict.scala create mode 100644 src/main/scala/pml/operators/Route.scala create mode 100644 src/main/scala/pml/operators/Use.scala create mode 100644 src/main/scala/pml/operators/Used.scala create mode 100644 src/main/scala/pml/operators/package.scala create mode 100644 src/main/scala/pml/package.scala create mode 100644 src/main/scala/views/dependability/executor/Scheduler.scala create mode 100644 src/main/scala/views/dependability/executor/Simulator.scala create mode 100644 src/main/scala/views/dependability/executor/Synchronize.scala create mode 100644 src/main/scala/views/dependability/exporters/AutomatonCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/BasicOperationCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/CeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/ExprCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/Folder.scala create mode 100644 src/main/scala/views/dependability/exporters/GenericImage.scala create mode 100644 src/main/scala/views/dependability/exporters/Model.scala create mode 100644 src/main/scala/views/dependability/exporters/PhylogFolder.scala create mode 100644 src/main/scala/views/dependability/exporters/PlatformCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/SoftwareCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/SystemCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/TargetCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/TransporterCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/TypeCeciliaExporter.scala create mode 100644 src/main/scala/views/dependability/exporters/package.scala create mode 100644 src/main/scala/views/dependability/model/Component.scala create mode 100644 src/main/scala/views/dependability/model/Copy.scala create mode 100644 src/main/scala/views/dependability/model/CustomTypes.scala create mode 100644 src/main/scala/views/dependability/model/Direction.scala create mode 100644 src/main/scala/views/dependability/model/EnumFailureMode.scala create mode 100644 src/main/scala/views/dependability/model/Event.scala create mode 100644 src/main/scala/views/dependability/model/Fire.scala create mode 100644 src/main/scala/views/dependability/model/Id.scala create mode 100644 src/main/scala/views/dependability/model/ModeAutomaton.scala create mode 100644 src/main/scala/views/dependability/model/Software.scala create mode 100644 src/main/scala/views/dependability/model/System.scala create mode 100644 src/main/scala/views/dependability/model/Target.scala create mode 100644 src/main/scala/views/dependability/model/Transition.scala create mode 100644 src/main/scala/views/dependability/model/Transporter.scala create mode 100644 src/main/scala/views/dependability/model/Variable.scala create mode 100644 src/main/scala/views/dependability/operators/IsCriticityOrdering.scala create mode 100644 src/main/scala/views/dependability/operators/IsFinite.scala create mode 100644 src/main/scala/views/dependability/operators/IsMergeable.scala create mode 100644 src/main/scala/views/dependability/operators/IsOptionLike.scala create mode 100644 src/main/scala/views/dependability/operators/IsShadowOrdering.scala create mode 100644 src/main/scala/views/dependability/operators/package.scala create mode 100644 src/main/scala/views/interference/examples/package.scala create mode 100644 src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneApplicativeTableBasedInterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneInterferenceGeneration.scala create mode 100644 src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystonePhysicalTableBasedInterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/examples/simpleKeystone/package.scala create mode 100644 src/main/scala/views/interference/examples/simpleT1042/SimpleT1042InterferenceGeneration.scala create mode 100644 src/main/scala/views/interference/examples/simpleT1042/SimpleT1042TableBasedInterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/exporters/IDPExporter.scala create mode 100644 src/main/scala/views/interference/exporters/InterferenceGraphExporter.scala create mode 100644 src/main/scala/views/interference/exporters/package.scala create mode 100644 src/main/scala/views/interference/model/formalisation/BDDFactory.scala create mode 100644 src/main/scala/views/interference/model/formalisation/ProblemElement.scala create mode 100644 src/main/scala/views/interference/model/formalisation/package.scala create mode 100644 src/main/scala/views/interference/model/package.scala create mode 100644 src/main/scala/views/interference/model/relations/EquivalenceRelation.scala create mode 100644 src/main/scala/views/interference/model/relations/ExclusiveRelation.scala create mode 100644 src/main/scala/views/interference/model/relations/InterfereRelation.scala create mode 100644 src/main/scala/views/interference/model/relations/NotInterfereRelation.scala create mode 100644 src/main/scala/views/interference/model/relations/TransparentSet.scala create mode 100644 src/main/scala/views/interference/model/relations/package.scala create mode 100644 src/main/scala/views/interference/model/specification/ApplicativeTableBasedInterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/model/specification/InterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/model/specification/PhysicalTableBasedInterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/model/specification/TableBasedInterferenceSpecification.scala create mode 100644 src/main/scala/views/interference/model/specification/package.scala create mode 100644 src/main/scala/views/interference/operators/Analyse.scala create mode 100644 src/main/scala/views/interference/operators/Equivalent.scala create mode 100644 src/main/scala/views/interference/operators/Exclusive.scala create mode 100644 src/main/scala/views/interference/operators/Interfere.scala create mode 100644 src/main/scala/views/interference/operators/PostProcess.scala create mode 100644 src/main/scala/views/interference/operators/Transform.scala create mode 100644 src/main/scala/views/interference/operators/Transparent.scala create mode 100644 src/main/scala/views/interference/operators/package.scala create mode 100644 src/main/scala/views/interference/package.scala create mode 100644 src/main/scala/views/package.scala create mode 100644 src/main/scala/views/patterns/examples/PhylogPatterns.scala create mode 100644 src/main/scala/views/patterns/exporters/LatexCodePrinter.scala create mode 100644 src/main/scala/views/patterns/exporters/LatexDiagramPrinter.scala create mode 100644 src/main/scala/views/patterns/exporters/allPrinters.scala create mode 100644 src/main/scala/views/patterns/model/PatternAST.scala create mode 100644 tikzStyle.tex diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 0000000..39a4ccd --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,13 @@ +* PML Analyzer Authors and contributors: +************************************************** +Main authors: + Kevin DELMAS : Kevin DOT Delmas AT onera DOT fr + Claire PAGETTI : Claire DOT Pagetti AT onera DOT fr + Frédéric BONIOL : Frederic DOT Boniol AT onera DOT fr + Julien BRUNEL : Julien DOT Brunel AT onera DOT fr + Thomas POLACSEK : Thomas DOT Polacsek AT onera DOT fr + Youcef BOUCHEBABA: Youcef DOT Bouchebaba AT onera DOT fr + +Contributors: + Nathanael SENSFELDER : Nathanael DOT Sensfelder AT onera DOT fr +************************************************** \ No newline at end of file diff --git a/LICENCE_PML_Analyzer.txt b/LICENCE_PML_Analyzer.txt new file mode 100644 index 0000000..84a04f2 --- /dev/null +++ b/LICENCE_PML_Analyzer.txt @@ -0,0 +1,24 @@ +************************************************** +PML analyzer is free software; you can redistribute it and/or modify it under +the terms of the Lesser General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your +option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the Lesser GNU General Public License +along with this program, in the file "lesser.txt"; if not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA + +All the library components are licensed under the GNU +Lesser General Public License. This is a GPL-compatible license. You should have +received a copy of the GNU LGPL with this program, in the file +"lesser.txt". + +All the applications components are licensed under +the Lesser General Public License v2 (LGPLv2) +************************************************** diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..455f843 --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +# Paths +BUILDDIR="build" +EXPORTDIR="export" +ANALYSISDIR="analysis" + +# Commands +BIBTEX=bibtex +MAKEINDEX=makeindex -s nomencl.ist +LATEX = pdflatex -interaction=nonstopmode --output-directory=$(BUILDDIR) +DOT=dot -Tpdf + + +# Compilable files +FILENAMES=$(shell grep -l -e \begin{document} $(EXPORTDIR)/*.tex) +REPORTNAMES=$(shell for file in $(FILENAMES); do basename $$file .tex; done;) +PMLNAMES=$(shell ls $(EXPORTDIR)/*.dot) +PMLBASENAME=$(shell for file in $(PMLNAMES); do basename $$file .dot; done;) +ANALYSESNAMES=$(shell grep -l -e \< $(ANALYSISDIR)/*.txt) + +# Compiling the patterns (TEX files) and the PML exports (DOT files) +all: patterns pml + +sortResultFiles: + @for file in $(ANALYSESNAMES); do \ + echo "Sorting $$file"; \ + sort -o "$$file" "$$file"; \ + done + +# Compiling all DOT files in the directory +pml: + @for file in $(PMLBASENAME); do \ + echo "Compiling $$file"; \ + $(DOT) -o "$(EXPORTDIR)/$$file.pdf" "$(EXPORTDIR)/$$file.dot"; \ + done + +# Compiling all TEX files in the directory +patterns: mkDir latex move cleanBuildDir + +# Export the code as a TAR archive +compress: + @tar -cf pml.tar src/ lib/ README.md tikzStyle.tex build.sbt Makefile AUTHORS.txt COPYING.txt + +# Export only pattern code as TAR archive +compressPatterns: + @tar -cf pattern.tar --exclude='src/main/scala/views/patterns/examples/ClairePattern.scala' --exclude='src/main/scala/views/patterns/examples/PhylogPatternsInstances.scala' src/main/scala/views/patterns README.md tikzStyle.tex build.sbt Makefile AUTHORS.txt COPYING.txt + +# Compiling all TEX files +latex: + @for file in $(REPORTNAMES); do \ + echo "Compiling $$file"; \ + $(LATEX) "\newcommand{\standalone}{} \input{$(EXPORTDIR)/$$file}" >/dev/null; \ + egrep -i "Latex Error" "$(BUILDDIR)/$$file".log || echo "Success" ; \ + done + +# Transforming all PDF to PNG +png: + @for file in $(REPORTNAMES); do \ + echo "Transforming $$file"; \ + convert -trim $(EXPORTDIR)/"$$file.pdf" -quality 100 $(EXPORTDIR)/"$$file.png"; \ + done + +# Extract TEX compilation results from the temporary build directory +move: + @mv $(BUILDDIR)/*.pdf $(EXPORTDIR) + +# Create the temporary build directory +mkDir: + @mkdir -p build + +# Remove the build directory +cleanBuildDir: + @ rm -rf $(BUILDDIR) + +# remove all PDF compiled out of the TEX +clean: cleanBuildDir + @rm -rf $(EXPORTDIR) $(ANALYSISDIR) + +.PHONY: clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..91f7119 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# PML analyzer + +The PML analyzer is an open source API providing a simple DSL to build +a description of the architecture of your chip based on the PHYLOG Model Language (PML). +From this representation a set of safety and interference model templates can be generated to perfom safety and +interference analyses of your platform. + +The only dependencies of the PML analyzer are: ++ The Java Runtime Environment version 8 [JRE 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html) or newer. ++ The Simple Build Tool [SBT](https://www.scala-sbt.org/) + +## Installing dependencies + +### Java 8 + +You need a working installation of the Java Runtime Environment +version 8 (either OpenJDK or Oracle will do). Installation procedures +may vary depending on your system (Windows, OSX, Linux), please follow +the official guidelines for your system. + +### SBT + +The compilation of a PML model can be easily performed +with [SBT](https://www.scala-sbt.org/). Installation procedures may vary depending on your system (Windows, OSX, Linux), +please follow the official guidelines for your system. + +## Using the PML analyzer + +### Overview + +There is no installation procedure for the PML analyzer itself, simply +create your own model by importing the ``pml.model`` package containing the basic constructors +of the PML language. + +The possible operation that can be performed on a PML model (such as linking +or unlinking entities) are provided in the ``pml.operators`` package. + +Exporters to [yuml](https://yuml.me/diagram) and [graphviz](http://www.graphviz.org/) are provided in the ``pml.exporters`` package. + +The compilation of a PML model can be easily perform with [SBT](https://www.scala-sbt.org/) ++ First launch sbt in the project repository (this operation may take some time) +```sbtshell + sbt +``` ++ Once sbt is ready to receive command launch the compilation and execution of your PML model +```sbtshell + runMain pathToYourModel + ``` + +### Editing a PML Model + +The PML analyzer is based on a platform description provided in a Scala embedded Domain Specific Language +called PML. Therefore, PML analyzer can be seen as an API to easily build your model and to carry out automatic analyses. + +Any IDE can be used to edit PML models, we can recommend [Intellij IDEA](https://www.jetbrains.com) that provides support plugins for Scala and SBT. + +Various benchmark systems for platform modeling are provided +in the ``pml.examples`` package. These benchmarks can be used as a starting point to +your modeling activity. + +#### Getting started with Intellij + +To edit PML model with Intellij please follow the installation steps given by [JetBrain](https://www.jetbrains.com). +The installation can be made for any platform and does not require any administrator privilege. +Once the Intellij is installed please download the Scala and SBT Executor plugins. + +#### Creating a project with Intellij + +The build specifications and project structure are provided with the PML source code. +So to create a project you simply have to select "Open project" on the starting menu of Intellij and indicate the directory containing PML (where the file ``build.sbt`` is). + +The tool should then configure automatically your project. +Please add all the library in ``lib`` as project libraries by right-clicking on them and select ``Add as library`` + +The last step is to indicate the Java version of the project, to do so please go to ``File/Project Structure/Project/Project`` SDK and select ``Java 1.8`` + +You are now able to build, run and debug your models with Intellij + +#### Troubleshooting + +**Connection error while loading project or running build** If your platform uses a proxy +please indicate the connection credentials in ``File/Settings/Appearance & Behaviour/System Settings/HTTP Proxy`` + +**No monosat library in path** If you want to use the integrated interference computation please indicate the path to the +dynamic library of monosat by editing your run configuration and adding to VM options ``Djava.library.path=yourPath`` + +### Examples + +#### Argumentation patterns + +The justification patterns considered for the CAST32-A are provided in the ``views.patterns`` package. +These patterns can be used as a starting point to start your argumentation activity. + +To compile and run the PHYLOG patterns example please enter the following commands: +```sbtshell + sbt runMain views.patterns.examples.PhylogPatterns +``` + +To compile and run the PHYLOG pattern instances example please enter the following commands: +```sbtshell + sbt runMain views.patterns.examples.PhylogPatternsInstances +``` + + +#### Modelling +Various benchmark systems for platform modeling are provided +in the ``pml.examples`` package. These benchmarks can be used as a starting point to +your modeling activity. + +To compile and run the Keystone example please enter the following commands: +```sbtshell + sbt runMain pml.examples.keystone.KeystoneExport +``` + +To compile and run the SimplePlatform example please enter the following commands: +```sbtshell + sbt runMain pml.examples.simple.SimpleExport +``` + +#### Analysis +For each view (interference, patterns and dependability) examples are provided in the dedicated ``views.X.examples``. +These benchmarks can be used as a starting point to +your analysis activity. For instance, we can carry out the interference analysis of the Keystone platform with + ```sbtshell + # example of a PML model where an IDP interference model is generated + sbt runMain views.interference.examples.KeystoneExport + + # example of a PML model where a Cecilia export is generated +sbt runMain views.dependability.examples.KeystoneExport + ``` + +If the tool is running on a Unix System you can use the Makefile to compile the DOT and LaTeX generated file: +```shell +# compile the DOT files +make pml + +# compile the LaTeX Argumentation Patterns +make patterns + +# transform PDF to PNG +make png +``` + +### Packaging + +All projects can be packaged into a single FATJAR containing all non-native dependencies. +The available projects can be obtained by running: +```sbtshell + sbt projects +``` +A project can then be selected by running: +```sbtshell + sbt project projectName +``` +To export a project as a FATJAR, simply select the project to export with the previous command and then run: +```sbtshell + sbt assembly +``` +The resulting FATJAR will be produced in projectName/target/scalaXXX + +### External tools + +The PML modeling does not rely on any external dependency. Nevertheless, it is possible +to connect some backend analysis tools to directly perform analyses out of your PML model: + * for interference analysis: [IDP](https://dtai.cs.kuleuven.be/software/idp/try) or [Monosat](https://github.com/sambayless/monosat) + * for the safety analysis: [CeciliaOCAS]() or [OpenAltarica](https://www.openaltarica.fr/docs-downloads/) + +The Monosat tool can be integrated as a dynamic library. To do so be sure that the library (.so for Linux, +.dylib for Mac, .dll for Windows) is accessible from the java library path. If not update it by running sbt or the executable with the VM option: +```shell +# to run SBT with a given library path +java -jar -Djava.library.path=yourPath sbt-launch.jar + +# to run a JAR +java -jar -Djava.library.path=yourPath youJar.jar +``` \ No newline at end of file diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..d8fb1a9 --- /dev/null +++ b/build.sbt @@ -0,0 +1,115 @@ +//Definition of the managed dependencies +val sourceCode = "com.lihaoyi" %% "sourcecode" % "0.3.0" +val scalaz = "org.scalaz" %% "scalaz-core" % "7.3.7" +val scala_xml = "org.scala-lang.modules" %% "scala-xml" % "2.1.0" +val scopt = "com.github.scopt" %% "scopt" % "4.1.0" +val scalactic = "org.scalactic" %% "scalactic" % "3.2.15" +val scalatest = "org.scalatest" %% "scalatest" % "3.2.15" % "test" +val scalaplus = "org.scalatestplus" %% "scalacheck-1-15" % "3.2.11.0" % "test" + +lazy val modelCode = taskKey[Seq[(String,File)]]("files to be embedded in docker") + +lazy val dockerProxySetting = ( + for { + httpProxy <- sys.env.get("http_proxy") + httpsProxy <- sys.env.get("https_proxy") + } yield { + Seq( docker / dockerBuildArguments := Map( + "http_proxy" -> httpProxy, + "https_proxy" -> httpsProxy)) + }) getOrElse Seq.empty + +lazy val dockerSettings = Seq( + docker / imageNames := Seq( + ImageName( + namespace = Some(organization.value), + repository = name.value, + tag = Some("v" + version.value) + ) + ), + modelCode := Seq( + "src/main/scala/pml/examples/simpleKeystone" -> (Compile / scalaSource).value / "pml" / "examples" / "simpleKeystone", + "src/main/scala/pml/examples/simpleT1042" -> (Compile / scalaSource).value / "pml" / "examples" / "simpleT1042", + "src/main/scala/views/interference/examples/simpleKeystone" -> (Compile / scalaSource).value / "views" / "interference" / "examples" / "simpleKeystone", + "src/main/scala/views/interference/examples/simpleT1042" -> (Compile / scalaSource).value / "views" / "interference" / "examples" / "simpleT1042", + ), + docker / dockerfile := { + // The assembly task generates a fat JAR file + val artifact: File = assembly.value + val generatedDoc = (Compile / doc).value + val artifactTargetPath = s"/home/user/code/lib/${artifact.name}" + val base = (Compile / baseDirectory).value + val binlib = base / "binlib" + new Dockerfile { + from("openjdk:8") + customInstruction("RUN", "apt-get update && apt-get --fix-missing update && apt-get install -y graphviz gnupg libgmp3-dev make") + env("SBT_VERSION", sbtVersion.value) + customInstruction("RUN", "mkdir /working/ && cd /working/ && curl -L -o sbt-$SBT_VERSION.deb https://repo.scala-sbt.org/scalasbt/debian/sbt-$SBT_VERSION.deb && dpkg -i sbt-$SBT_VERSION.deb && rm sbt-$SBT_VERSION.deb && apt-get update && apt-get install sbt && cd && rm -r /working/") + customInstruction("RUN", "groupadd -r user && useradd --no-log-init -r -g user user") + customInstruction("RUN", "mkdir -p /home/user/code") + customInstruction("RUN", "mkdir -p /home/user/code/lib") + customInstruction("RUN", "mkdir -p /home/user/code/src/main/scala/pml") + customInstruction("RUN", "mkdir -p /home/user/code/src/main/scala/views/interference") + workDir("/home/user/code") + for((to,from) <- modelCode.value) + copy(from, to) + copy((Compile / doc / target).value, "doc") + copy(binlib, "binlib") + copy(artifact, artifactTargetPath) + copy(Seq(base / "AUTHORS.txt", base / "lesser.txt", base / "minimalBuildSBT.txt", base / "LICENCE_PML_Analyzer.txt", base / "Makefile"), "./") + customInstruction("RUN", "mv minimalBuildSBT.txt build.sbt") + env("LD_LIBRARY_PATH" -> "/home/user/code/binlib:${LD_LIBRARY_PATH}") + customInstruction("RUN", "chown -R user /home/user && chgrp -R user /home/user") + user("user") + customInstruction("RUN", "sbt \"compile\" clean") + entryPoint("/bin/bash") + } + } +) ++ dockerProxySetting + +lazy val docSetting = + Compile / doc / scalacOptions ++= Seq( + "-groups", + "-siteroot", "doc", + "-doc-root-content", "doc/_assets/text/rootContent.txt", + "-skip-by-regex:pml.expertises,views.dependability.*,pml.examples,views.interference.examples,pml.model.relations,views.interference.model.relations", + "-project-logo", "doc/_assets/images/phylog_logo.gif" + ) + +lazy val assemblySettings = Seq( + assembly / assemblyJarName := s"PMLAnalyzer_${version.value}.jar", + assembly / assemblyMergeStrategy := { + case PathList(ps@_*) if ps.contains("patterns") => MergeStrategy.discard + case PathList(ps@_*) if ps.contains("examples") => MergeStrategy.discard + case x => + (ThisBuild / assemblyMergeStrategy).value(x) + }) + +//Definition of the common settings for the projects (ie the scala version, compilation options and library resolvers) +lazy val commonSettings = Seq( + organization := "onera", + version := "1.0.0", + scalaVersion := "3.2.2", + sbtVersion := "1.8.2", + scalacOptions := Seq("-unchecked", "-deprecation", "-feature"), + resolvers ++= Resolver.sonatypeOssRepos("releases"), + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), + libraryDependencies ++= Seq( + scalaz, + scala_xml, + sourceCode, + scalatest, + scalactic, + scalaplus + ), + docSetting +) ++ dockerSettings ++ assemblySettings + +// The service project is the main project containing all the sources for +// PML modelling and analysis +lazy val PMLAnalyzer = (project in file(".")) + .enablePlugins(DockerPlugin) + .settings(commonSettings: _*) + .settings( + name := "pml_analyzer") + diff --git a/doc/_assets/images/phylog_logo.gif b/doc/_assets/images/phylog_logo.gif new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleKeystone/interference.PNG b/doc/_assets/images/simpleKeystone/interference.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleKeystone/platform.PNG b/doc/_assets/images/simpleKeystone/platform.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleKeystone/temporal_slices.PNG b/doc/_assets/images/simpleKeystone/temporal_slices.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleKeystone/transactions.PNG b/doc/_assets/images/simpleKeystone/transactions.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleKeystone/transactions_footprint.PNG b/doc/_assets/images/simpleKeystone/transactions_footprint.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleT1042/interference.PNG b/doc/_assets/images/simpleT1042/interference.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleT1042/platform.PNG b/doc/_assets/images/simpleT1042/platform.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/images/simpleT1042/transactions.PNG b/doc/_assets/images/simpleT1042/transactions.PNG new file mode 100644 index 0000000..e69de29 diff --git a/doc/_assets/text/rootContent.txt b/doc/_assets/text/rootContent.txt new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/export.md b/doc/_docs/example/simpleKeystone/export.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/index.md b/doc/_docs/example/simpleKeystone/index.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/library.md b/doc/_docs/example/simpleKeystone/library.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/platform.md b/doc/_docs/example/simpleKeystone/platform.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/routing.md b/doc/_docs/example/simpleKeystone/routing.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/slicing.md b/doc/_docs/example/simpleKeystone/slicing.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/software.md b/doc/_docs/example/simpleKeystone/software.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleKeystone/specification.md b/doc/_docs/example/simpleKeystone/specification.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/example/simpleT1042/index.md b/doc/_docs/example/simpleT1042/index.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/index.md b/doc/_docs/index.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/gettingStarted/external_dependency.md b/doc/_docs/reference/gettingStarted/external_dependency.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/gettingStarted/getting_started_SBT.md b/doc/_docs/reference/gettingStarted/getting_started_SBT.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/gettingStarted/getting_started_docker.md b/doc/_docs/reference/gettingStarted/getting_started_docker.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/gettingStarted/index.md b/doc/_docs/reference/gettingStarted/index.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/usePML/editing_PML_model.md b/doc/_docs/reference/usePML/editing_PML_model.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/usePML/index.md b/doc/_docs/reference/usePML/index.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_docs/reference/usePML/pml_examples.md b/doc/_docs/reference/usePML/pml_examples.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/_layouts/base.html b/doc/_layouts/base.html new file mode 100644 index 0000000..e69de29 diff --git a/doc/_layouts/doc-page.html b/doc/_layouts/doc-page.html new file mode 100644 index 0000000..e69de29 diff --git a/doc/_layouts/index.html b/doc/_layouts/index.html new file mode 100644 index 0000000..e69de29 diff --git a/doc/_layouts/main.html b/doc/_layouts/main.html new file mode 100644 index 0000000..e69de29 diff --git a/doc/_layouts/static-site-main.html b/doc/_layouts/static-site-main.html new file mode 100644 index 0000000..e69de29 diff --git a/doc/sidebar.yml b/doc/sidebar.yml new file mode 100644 index 0000000..e69de29 diff --git a/lesser.txt b/lesser.txt new file mode 100644 index 0000000..f166cc5 --- /dev/null +++ b/lesser.txt @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/minimalBuildSBT.txt b/minimalBuildSBT.txt new file mode 100644 index 0000000..088ca65 --- /dev/null +++ b/minimalBuildSBT.txt @@ -0,0 +1,10 @@ +lazy val root = (project in file(".")) + .settings( + name := "myProject", + version := "1.0.0", + scalaVersion := "3.2.2", + sbtVersion := "1.8.2", + scalacOptions := Seq("-unchecked", "-deprecation", "-feature"), + resolvers ++= Resolver.sonatypeOssRepos("releases"), + resolvers ++= Resolver.sonatypeOssRepos("snapshots") + ) \ No newline at end of file diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..46e43a9 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.8.2 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..9fdd2f7 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") +addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.9.0") \ No newline at end of file diff --git a/src/main/doc-resources/pml/examples/simpleKeystone/SimpleDoc.md b/src/main/doc-resources/pml/examples/simpleKeystone/SimpleDoc.md new file mode 100644 index 0000000..3a1b926 --- /dev/null +++ b/src/main/doc-resources/pml/examples/simpleKeystone/SimpleDoc.md @@ -0,0 +1,149 @@ +# Documentation for Simple example + +## Platform + +This processor is composed of: +* two cores, +* one DMA (Direct Access Memory), +* one Ethernet device, +* one DDR and SRAM memory, +* one SPI controller, +* an interrupt controller (MPIC), +* and a set of configuration registers reachable through a specific configuration bus. + +As shown in Figure 1 the two core are linked by an +AXI bus. The IO devices (DMA, Ethernet controler and SPI) are +linked through a dedicated peripheral bus. These two buses are +connected to the memory subsystem (containing the DDR and +the SRAM memories) through a dedicated controller +called Memory Shared Multicore Controller (MSMC). This controller +acts as a switch from the two buses to the two memories. + +All the resources necessary for executing program instructions are locally hosted by each core: ordinal counter, registers, computing units, etc. +These resources are private to each core. +They can be used simultaneously without interference by each core. +Conversely, the memory hierarchy is composed of resources local +to each core (the cache memories), and also global +resources (such as DDR and SRAM) simultaneously reachable +by the cores and the IO devices. +These global memories are shared resources. + +![platform](platform.PNG "multicore processor") + +Figure 1: Multicore processor + +PML Encoding is provided in src/main/scala/pml/examples/simpleKeystone/SimpleKeystonePlatform.scala + +## Software Allocation + +The application layer is composed of five tasks: +* app4 is an asynchronous microcode running on the eth component. +* app21 is a periodic task running on core2. +* app22 is a periodic task running on core2. +* app3 a microcode running on DMA. +* app1 is an asynchronous applicative task running on core1. + +PML Encoding is provided in src/main/scala/pml/examples/simple/SimpleSoftwareAllocation.scala + +## Transaction library + +The application layer is composed of five tasks: +* app4 is an asynchronous microcode running on the Ethernet component. +Each time an Ethernet frame arrives, it transfers the payload of the frame to SRAM (transaction t41). +* app21 and app22 are two periodic tasks running core1. + * * At each period app21 reads the last Ethernet message from SRAM, +makes some input treatments on the message, and makes it available for app1 in DDR. + * * Similarly, at each period app22 reads output data of app1 from DDR. It transforms them into SPI frames. + The frames are then store in SRAM. And finally app22 wakes up the DMA (app3) + by writing the address of the SPI frames into the DMA registers. +* app3 is a microcode running on DMA. When woke up, app3 reads the SPI frame from +SRAM and transfers it to SPI. +* app1 is an asynchronous applicative task running on core0 and activated each time a external interrupt arrives. +It begins by reading the interrupt code from MPIC (transaction t11). +It reads its input data from DDR (transaction t12). +Then it runs using the internal cache of core0 (transaction t13). +And finally it stores its output data in DDR (transaction t14). + +The transactions are drawn in Figure 2. + +![transaction](transactions.PNG "Transactions of app1, app21 app22, app3,and app4") + +Figure 2: Transactions of app1, app21 app22, app3,and app4. + +By design, app22 and app3 do not run simultaneously, as app22 wakes up app3 at the end of its execution. + +PML Encoding is provided in src/main/scala/pml/examples/simple/SimpleTransactionLibrary.scala + +In this example all defined transaction are used, the configuration of the library is provided in src/main/scala/examples/simple/SimpleLibraryConfiguration + +## Routing + +In this example, as shown in Figure 1, there are multiple paths between the cores and the configuration registers. +These registers can be reached from the core either through AXI-BUS, MSMC, PERIPH-BUS and CONFIG-BUS, or directly through CONFIG-BUS. +The platform is configured such that the read and store accesses by the core to the configuration registers are routed through the +the direct path. + +The PML routing rules are encoded in src/main/scala/pml/examples/simple/SimpleRoutingConfiguration.scala + +## Temporal slices + +The tasks are scheduled into two periodic time slices as shown in Figure 3: +app4 and app21 are scheduled in the first time slice; +app22 and app3 are scheduled in the second time slice; +and as app1 is asynchronous, it can run at any time, that is, possibly in the both slices. + +![slices](temporal_slices.PNG "temporal scheduling") + +Figure 3: Temporal scheduling. + +The footprint of the transactions of these five tasks on the architecture is shown of each time slice in Figure 4. + +![transactions_footprint](transactions_footprint.PNG "Footprint of the transactions on the HW architecture (the red, violet, blue, and green arrows represent respectively the transactions of app1, app2 (app21, app22), app3, and app3).") + +Figure 4: Footprint of the transactions on the HW architecture (the red, violet, blue, and green arrows represent respectively the transactions of app1, app2 (app21, app22), app3, and app3). + +The PML encoding of the first time slice is provided in src/main/scala/pml/examples/simple/SimpleKeystoneLibraryConfigurationPalnApp21.scala, +and the second time slice is provided in src/main/scala/pml/examples/simple/SimpleKeystoneLibraryConfigurationPalnApp22.scala + +## Specifications + +In this example we consider that +* bus services are independent +* DMA and dma-reg services impacts each others +* app21 and app22 are exclusive as they run on the same core +* app22 and app3 are exclusive as app22 wakes up app3 at the en of its execution. + +PML encoding is provided in src/main/scala/views/interference/examples/simple/SimpleTableBasedInterferenceSpecification + +## Exports + +### Configured platform + +The file src/main/scala/pml/examples/simple/SimpleExport shows how graphical exports are produced (stored in export folder) +from a platform: +* graph of used SW and HW +* graph of used services per application +* table of transaction +* table of data +* table of SW allocation to HW +* table of component activation +* table of SW usage +* routing table +* transfert table + +### Interference analysis + +The file src/main/scala/views/interference/examples/SimpleInterferenceGeneration shows how interference analysis can be performed +on a configured platform. The generated files are stored in analysis folder: +* computation of n-itf +* computation of n-free +* computation of n-channels + +As an example the following interference is identified as a 3-itf in the first time slice: + +< app1_wr_d2 || app21_wr_d1 || app4_wr_input_d > + +![interference_channel](interference.PNG "Footprint and interference channel (identified by the two circles)") + +Figure 5: Example of footprint and interference channel (identified by the two circles) + diff --git a/src/main/doc-resources/pml/examples/simpleKeystone/interference.PNG b/src/main/doc-resources/pml/examples/simpleKeystone/interference.PNG new file mode 100644 index 0000000000000000000000000000000000000000..14dc0ddefa3cae2dddf83c7f317a76c26560875b GIT binary patch literal 71823 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 literal 0 HcmV?d00001 diff --git a/src/main/doc-resources/pml/examples/simpleKeystone/platform.PNG b/src/main/doc-resources/pml/examples/simpleKeystone/platform.PNG new file mode 100644 index 0000000000000000000000000000000000000000..063291d142b55fd293a24e38deda334c9a9c0721 GIT binary patch literal 47942 zcmag`cRbbq|38j5gp+j~%BEw>mYI-akL;b1LntF7dsPmOy;oVs-XgN8Y>JeTofX-8 z)AxS#TF=kr{k#1B;2h^X9{2n0aogj17owr2Ky;n@`lU;kh?Ep%wJu${3cqyeiX}cS z_ym`bCmFn4cF|HmTq^6M{SE%Wv6g-yed$s~6v3(aHSjmYQPI%l(k0?n?9b)SX9bp* zF4d1K$x7>Zn69P~`55b_9lVn)F798KT-KXItJn``>0E!0I~*jROIH8QkU&uD2kzUT zTjRosRUMQKPb}8d>WyOco@$}e-XmV~G4MV}3^KQ7Am@!=>a0w3;)f`Q-Ix80oa z*bl&4MN`bhci`9db(Vj$L|zb=b&Hifnb=G*qs zSZ7hXBZlf5;t{0~n=xKR**+m!b|*I9ezi?6HTvr4VPhrI3n$|s%i3CetcGJiC%7+v zo*26?`HgrelJph6YUD@nDw1^;7IqF&eug#c`Sn`W4|iNuuNm!=eDrpMUB0WRjH6zs zIpc^qD|Fl=NPViX7fY?ft1&fA$imG0M#6?PD%Zz!UE5h4OK|9QW&8@-DQ*@=Zl}Ts zoz5V*oI1=gkFLyfCl^&4V}7SjR7~WBSMYZ8MoKizC(UbejV&q4Xjz|*Kz4XsF!g|k zjaDL-EwMa+J~7#=L%wfGT#IxBk?b!6!|jS+T1UNs#a@nU66*1$C|~4w(QKsL)pk`) zXCwcDb18sXi82Y%+LsASh=p`kh&#FoA;g%+t_)Z5+2JYB*eiGwPR@c?f|tXL5v&96 z8vhE!@-mieI7IVw)`oxnKQVY7CW2@zP=fD&_6(KeK?-7A5QjN z=(;AN3jX9mh|DhR@HS@~*p={1-*lUjyE9b!?sCxk)60op9ZM~lEx(1O+wT<=m�T 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 GgZ>BaZUDys literal 0 HcmV?d00001 diff --git a/src/main/doc-resources/pml/examples/simpleKeystone/transactions.PNG b/src/main/doc-resources/pml/examples/simpleKeystone/transactions.PNG new file mode 100644 index 0000000000000000000000000000000000000000..0c5169a7dd383d016bb60a5d54e2993f4251d98c GIT binary patch literal 101345 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�ABVOcpp6h#uCPW}iAmUL@S{j`QVxc1gkh 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 YYq~0wUabY1^a~fX)bvzKpjOZR59I+z>i_@% literal 0 HcmV?d00001 diff --git a/src/main/doc-resources/pml/examples/simpleKeystone/transactions_footprint.PNG b/src/main/doc-resources/pml/examples/simpleKeystone/transactions_footprint.PNG new file mode 100644 index 0000000000000000000000000000000000000000..6404603993cb17a783525aead41152eefffd55fa GIT binary patch literal 80327 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
  • vk z5CPKB0%9;Qu(NZBiiyj}%JK2>n_Jl`tEd?moBH^NxO(_R#U=YU?hR<(8`gdxBc~`a zH7l>Uva^5sgz599&s(;1&DJ%W_wGD&=D^7-$1mJIcjNKZ`>*ai`^3n^3_+~53l8_B zWX@ZBVr|areXlu+uO>?fm=jS}nAN9DJe$7&)u7rC4gkv?Uyva`i&B#RD8!rwF?x b>PzuwM{r9TPq4OLwAhVTW93R-MFwjCE+1pW literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/function_green_circle.gif b/src/main/resources/icons/generic/function_green_circle.gif new file mode 100644 index 0000000000000000000000000000000000000000..c6974e07d0640aea2c5e08060cfe76ccfe0250f3 GIT binary patch literal 284 zcmZ?wbhEHblw=TKC}%KK{3jKhpH@vk z5CPKB0%9;Qu(7iWiSkRxitzAp$*D*fTB>UsDcO1$I{TaX$2vr3dxoaCCKmZs_9fO& zO>3Q#J#lsE^i5S$F9c7z5U}mqq*cc|_S~Jm4P1!iHE3u~Meo?l|&`Y=_wVZt^kMy^U}F_s!(ZE<_19Q_b!Nq+~{$pWqkdSX0T W;oMSI()|k-ELt3|v0|kngEatz^jr)8 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/function_purple_circle.gif b/src/main/resources/icons/generic/function_purple_circle.gif new file mode 100644 index 0000000000000000000000000000000000000000..1ec55d402627fbe0a4bca2ccb8adc9361ea10d04 GIT binary patch literal 273 zcmZ?wbhEHblw=TKC}%KK{3jKhpH@vk z5CPKB0%9;Q@bmG>$w+gsvx|$0+M1iIDJ$t|YP#6jh4}dRxVlCM2PH>ER_5hqB_7AYH=FQ!@bjgv;o6hXq`RUH>S68n*K6jRpiJ7(F zfk51a7R`rUblDyDQ+h=*DmA9 T%~K|aPO}lxm@`L6k--`OD7s^n literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/function_red_circle.gif b/src/main/resources/icons/generic/function_red_circle.gif new file mode 100644 index 0000000000000000000000000000000000000000..0c48fe317a5628b2d8aa5577b1533e3cd96077eb GIT binary patch literal 274 zcmZ?wbhEHblw=TKC}%KK{3jKhpH@vk z5CPKB0%9;Q2yk(6u(C=B2xv%2DTs<1$je7qSvhEF1(=w4=<6rg+t&vN7kGMB`1@zL zxb(-xwMRtE&&b$bQL#KXcYkf|`Hqh3y}kc9IsY>-{9|T*KX>l`<;%Y>TExi2%vzYR ztT!cd-r^f;b6)Rz&2fG+PqpAA4WaPt4c>|lDqI={(cQ@$B3fdnlnf5+nX<-B;b9Sr zK==Y}!3{es7#)9@?%83}eBjnm4x5S=X7A+L6(T#FpaTV}P(ii7)<@D$^ SnWh^t-Bem*?p$d_25SJ4I9fdb literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/function_red_cross.gif b/src/main/resources/icons/generic/function_red_cross.gif new file mode 100644 index 0000000000000000000000000000000000000000..25eadb7ddffbf4a0c09073712b5445ff86128416 GIT binary patch literal 427 zcmZ?wbhEHblwuHKC}%KK{3jKhpH@vk z5CPKB3SuxYaIv#%NJ$yU%Uh_bhA}gHFfcf1X?f`DM_5^fSy~2|m}D?8Bmj}UeTBb& zeQB-6485p)#R4mWU-CSC_EGK7uaq)2mhU>k($D5k&PnvYTqvIbZ z=YIx<_Y4gGn3evMcW7P*}ML=k35&dHUt5~Y~9duOKS@)roRC@fr=P$m)=S(C6xOOUlC zE|0aaZ=+VEV5OE^%At~2(doxyHTZd^6s4vk z5TV8(#Lx<2Ffed(aq;sDNJ-0R>*~9D`1myJ4r$%z-?%rZd0$||{=kOaflYh;8umtZ z9E|D&q61MK2O~NTM7JG?Y&(!wT$z-fla^JGHtA^gwBvP)&$stYU9n;3hGP%6oP50Z z{IgS+?;SXC`P`kiSMR^NeD~#{%g^sT`*iui`|FQCFfuVyh}ha47#=iwNc5jrQj{6u z(9+P6Ua~4`{f#x-x_+fd@SiX9xz?n@6;ip^Y}5T@^P0M;msd}7SP;DJ{zvgz&J;x^ zMrNf-770cc6}IZ8l*%d@vBv44Y&DaTXEvzM&##MYt&*KNQvk z5TV8(#Lx<2Ffed%vGekCiAxKq>B`%B7>1|0MP+#;7WtL;BurUXKJjYkl#8L0F9lD! z5HR^d;M7au6E6n#oB=YzW?qh+d@gY6#qdc$@rxnTFNROO96IMp!lLV$oA0)4x!=C& zV*kox9eeIh-*tQ1zQvk z5TVW>#Lx<2FfeelvkP)_%ZZ2>DJlk-nA8Uc7kPV^`TEXHPe0tyaBt$o{|pTOczOS^ zvHfFZ{l~-ePfF?^H}^j#rvE%V|2a7RDJlK4wESOO{J)~&|Mcnq=g$4#)bwiR%>Nx7 z|CcZSzG%_c1q=Q!TExi2Om)Q8=D={^V}?Zki6upqEiNsG&#<|yjaq*rXX@gKE?XaL z*k;^VaOA}n!?S$rZ-01g9cRnJ#QkwjocjyuBsnHVW}Y$@PDU0*wu<_svT`o=x+x)S zmHmm+LZ;2hsg7(a=bl%m#LAViuqLZvZ2_-{@`hYqVfn3@(yrp^yAqToL>a^cd`_J7 K5;@|?U=0A#4|ItD literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/orBlue.gif b/src/main/resources/icons/generic/orBlue.gif new file mode 100644 index 0000000000000000000000000000000000000000..cb9bd9a584d7365421e9075fa2133be1e21476e2 GIT binary patch literal 378 zcmZ?wbhEHblwc5LC}%KK{3jKhpH@vk zMg}GZaRwoVRv^bBw#zcE$2_vbGPcViuG2EM!#c9VJhsCwae{GLhjmhmZ9=zQQm<`7 zk8A2g=hO*qS(9DUCxw*Ea!sA=oIb%ZrQb1mfp$hLp_q%b($p z+#g%BD6hD(xNk#2_qx*l4atp5YNu~+@0;4SXkYi@z4JDmp0n}9lvPL8?Z3Ks``LYG z?(H~!`_jEPXKubYbn(>55Gd`*i)$J4PmET!^jmfJ=ghj#PUSdje-eN7jWEC0W6) zGZ;LxPt9Go?u=(S&(s7-!=mz*+7(w~#KjtBwfQ{>Z_QM=`7zmuH$t^Oi_wQgp)IAu zN4Y1~kk5y;G^jkqSjf?%}>$$}WE2+cJ?1~NXf8v{+`eBAfA$@@4m GSOWmvk zMg}GZaRwoVRv;&_$gjL7Ve*CGNf!bpT?m|dDQx1^(4I4alP`x&yA(bZh%Sdsycjt1 zO3bv2VUsTeOgI6mE(T4z5H|Bt)U?atQ!j?kx)M9{ za^#eYLG!LA&$*hk0c4Vqvk zMg}GZaRwoVRv@Q7ICyS)`ad?d{|pTOn3(=?asB1y{>RV%kB8@^97#Vlw`rY^YdKGBf}~Q>_9oMcD|ZCM7R+R0c@o&{x#7mgWEJKJ z@%k*58i}@)4j;+xSS6mC(xCDbRWENo=?O9NoId<&^MYj}xC>$gB4k$usaJa^tq)KW Os&U`t%J1XIU=0B6{dd9u literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/preBlue.gif b/src/main/resources/icons/generic/preBlue.gif new file mode 100644 index 0000000000000000000000000000000000000000..49af964b62d62b9df123d3bfb711261a2cb806a6 GIT binary patch literal 445 zcmZ?wbhEHb)L@WeC}%KK{3jKhpH@vk z5TU^!$*_nXdb)b}_%!SeY2D}FxHqVIUtq)jz=qv{O?&+s z_C|IbjOqlU15q6ZBRUR5w;hOVJCIjgnUtQBHtA^gwBvP)&rg^>uf1>ThGP%6oP50Z z{IdfmE+0F8^W2@cSMR^NaP#S*%g^sT`*iui` z!zNjAiJ9w~w{G5`%DO#Z=UT45Wh<8L+_CFe@7B5vN-Tvk z5TVK-!O#j~Ffj1)bBRj}iAf2m>B?KX>f3r4hNrnD7WvgrO)Kw7n0PgG%Ei#hmx3o< z2$*~!aO$P-i5CNV&Hx!&39Y2+;3lb ztYgpJ>D#YQUU#-<-{YzKA5Y)@plAPsSvwx|9eOh7;FDR5Ow5!Zwyh2g1s*zl%pQx2 zPTDBVZ(J6_S?abt!eP^oGhW->HcYSLoj#lQc=Fl2-S}T!K(72 z47_sn?H#ph;sUCj{hiqpCf84zGQDbAJ(mZQ!W>3sMNW@J9kc4V*wooMR;}UYfp=1p4;w6CvRrOcvmtbc3GDb4kpmMm19uc1x= literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/prePurple.gif b/src/main/resources/icons/generic/prePurple.gif new file mode 100644 index 0000000000000000000000000000000000000000..4368128961412b9d6aaa0fd20a2b1935cfa585fe GIT binary patch literal 438 zcmZ?wbhEHb)MSukC}%KK{3jKhpH@vk z5TVH+&d>^CFfa)4^UFv{$w*4-YioNtIr+G{R_5jABqdEWHlAT?J=NTFhKWIxNrVUDZw94DvQPL8u39cDW@%$v|ZwY_~&p#O@9@GbdyS63{* zv3AY99osJ-*njib(Fc3?ygYOI)1BLIE?#(b^~$@OH=bTN|NiA zA!ddpg(q{wm@lljMLnG6CQ4Ty*5a!XG_)Dyxli@PhI$=IrH6*J$2`= z&v%ev5RtEKZEI5$=T&d-?JLjhpVT&S^0ca{tx^F@O0yZ6H8=tm_RXx7V$)>jT)A4B zgLCronssYdYO!tz*tCqNeg4Avn>KFV-?pxNr80}s(MemXPpYlkxOjoazUj9o9KSP- kM^O9z0|}KUlT^8dWP4t;Ys*P82=nHD|B)~F*^$8-0PfqN7ytkO literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/preRed.gif b/src/main/resources/icons/generic/preRed.gif new file mode 100644 index 0000000000000000000000000000000000000000..165faf8bac2fb46def62e2fec337feee19700168 GIT binary patch literal 437 zcmZ?wbhEHb)L@WeC}%KK{3jKhpH@vk z5TU^!!O#j~Ffa&mbIXZ{$O;P^DJuFJ83mY_)CUI_d3(=IPhXy!dvD^z{|pTOczOS^ zvHfFZ{l~-ePfF?^H}^j#rvE%V|2a7RDJlK4wESOO{J)~&|Mcnq=g$4#)bw%QyrqfS@M6;B1R@=>LRu+4h;n!I(*C?i;GU0D9t~)Y|2j&r{xjp2WGxy zTI9N6yQW><-ahx_vw6Gke=t)qDoHV0xTo&?``_PXq=b0O>f74eWQ7G}I{Nw*vnNb$ zn>1y5^|W?w4DjLb?L9*a6=wQ;j4vvaOm!^OclVMYD=wX5Vo+Y~sI-5^orx#zPT>_%dGJtJ;pqhFEb-o#?J5#ZVgiNV Mz88o%IWkxS0Nj<49RL6T literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/select1Green.gif b/src/main/resources/icons/generic/select1Green.gif new file mode 100644 index 0000000000000000000000000000000000000000..b0da60dfa3ffc3fe4f616ae2ab405ab7a2b687f7 GIT binary patch literal 1136 zcmeH@*-z2|0DylW%_BE+np;T;QEqK+;?|+{V$D2CrF6L-Y|D8}H~n5%cGfif|C6ez4YyS`Ch-Uh|s{GgbRQtkP8GLkI%5^L34Ra&3U~wV0P$}DKuvExz5NBfBZnix z)eqvBqFicgHmB!x!w{~5pIIfqmN#XTH9n{CGAmoM7=oO);ygx24xSf((y zx+|})zksEQVQ3!G)eq?^3{&%%rHQ4fFcni#WwHlE@IFa$pF9ysosOofF*GHHs)(j3 zqnn0^&11#f9#Z2-QAd9RcchRzT-c|jjA+??Ul=3H?DwB(d}&GV9HV8NIP{ezR#);Q zgzgz?>jZIlnf>8=wQ!m|xX2P~s6qvKV1X%Al1H_ifkmchn$o|(>{L~>PLOz#Vjf6n zmy?FRv3h3d{qt3WlP$xa1Y^3IQ5|PY$B}5cilsiy{M6FY(%jsf)L183sRi{lpuP?? z)JpXAprJyPOM7|CGP=ncpm@`rHipt}~skzJZTvfOwRmCf{DV@``f@ zMXqJym)UKG-+VdSTRPj7nK*vF4jM0Y^Y<4sv@#JZbn>81fZuhh$xgV z1;c$ReKIp6BNjS1wSJVet;$*cFJ(POba&!C#I>TX@L+BUJUH7tE6#Sjdhtxb(Hjmj ziR5<5(JOV45vFdK2s|K=)^A3h1w-QDFC~2HJ9W67gxC-pDVIW@p;L7_2n+_IE)N70 zVK>lWw99441ZatimtQQ-h{(>_~N;DLttL1xD9+2UX5bD#U%eL|@r0Y_t9VH;$EkGsX?Y)azI z%-HmVV{Q@GwYk|Om11f&rc#+@vK6HgQz(p0O$%zZv7^Hvkt{bh8s&16OomCNIEw1U zViSt4py*$*81L)DMn)DVCKfari%y5@^=5+s*Xc|vE4ayoVVK2Y`8_jZG8pFP=k zy-kjZ40$o!sSJ+t+q~tHCi&ga@P|=6C^xHpxm@GNukpgMTD z2O-P@9%mc1_hP1P2##^>>>U)ZU5Cj}Z=Y)x6Dh?6mpAP`8S5izICCvXQWf%gldH4e z<-Msk%o^XMm>XS(+MMlo?+#Btmvk zMg}GZ83sXyHV}`2pXl63i+-#&VtE7x(Qwux{6W<;Q2w zJim1D(U~(3PM>~w_RN#>=N_Ip^Wnk$kB=Vy`SIh=uU}tYz54U}*YEG&Uq5^H>*vpx zPoI2!_3H1RKc8Q|eE<6O?_a-OzkdDg+qb`e|Ni^;@5z%VpFe;8^XJdgr%%6p`SSn& ze?}%|vJw0L20mtH78ZWi*5laS6b#iuzSeYH^YPW9v+>MJJ-3%3#qIP7jUb}D3fhA7v$|?dz(K~j{I&*G{ zhq0=Ppis<-SWm}&=WHCkJWSNog+)x`cEr2c|M=nP$g*hCWV>lR8ygonv2Z6`YY{YV z^N~!+p7G;#h?mD?X|y#3d$e z5pd(=W@lDLRvh@h)Z%X6c_}Jk}xP6O4XOT-+ lI81-n#LDaP^6u&F`u80etO3eqX#fBK literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/select1Red.gif b/src/main/resources/icons/generic/select1Red.gif new file mode 100644 index 0000000000000000000000000000000000000000..060f6e3c07ab6864dd4fafaca88de04f5d814ace GIT binary patch literal 730 zcma)x>rYYv9EN{_E}WM#&9c&=xpgosJ{4;HoACMqm1WT4=1 z^8$&Y9z$!a4>A{;mb!9T%~JDLjnFhSDG-6ff#^Tz-Sgpn_QWTgqekZjfghAWWUzgI zT5-YMl3Z4P3^}PNk5zofsboe$K~Y{l1Y2BPz`1Y&5a0m#K=nTZ7-F+ul$EJVO9$EP zp|Y}(^77&G@{x*)mlYLrb#;^V^@~E`L}le?fnd0%W=SM^&*Qz~aAx@YHh-in);y+}+&)U5SU=u(GumNBIusRcwhyhptFd`Ap%-obpbuBHwB$Cyprk~Bt+QvphTiaT5 z^IB8WhD4$fi}h0J59e=fwRd-8ot;*-+Nx67y1E=Fx~))HQ55g)w#en{YPF@m-`vq* zl*up@wRU!z<#JPd`<6^*otoNMSXiH*w@gjh^?KZ7vYE}eUXN{W;~4f^qruE(ola*m znQ$Dp+wJ)3s#dEt7z{R>4O>}RUS2jDjoVtS!{Klt5dViA4$fhO5=K1|MnP&H@&!mZ z01{72U|l<(7*Cf-AGIMON@CJ=7Mgv5(c1Qe-VT)H=#(6km77ZIdn#3irNwx7URJRl z_YO*w&tkp22_a0?K+D8iT6)}0Z{n^ic) z@j58<`mRRiikM^MfY8E-lQNXyy2pQ5mSqU_4?Z1rYgqV=ljar}cZ_%H)5lTDnOb%x zbljYE3%>Wd$NGTj9}>&&$#{OB)EFQND5TyHcbpXjMEzNjM#{$oG!D@th)zuvk zCLkfrAj;4N;xRDf&9y0BVqde-rFy+{%bMusT^=*m)%Pwd>|0*kzoK~3n$nqDs=F5# zOjubwX;tZ@HD#06mQ7h(K5awgob9zU*49l~S3YZN&HUZZ0%)=M99J{&k(33d_AJ0DU zX!e1Jvkp9*bMV2u1CM5%dAk1elXa(`uD}28-2HcF??1bK`O(2gA1^(4e&gT2fA79O z{rdCMuV26Z{{8#*>ys}(-~Igg^VP>kKYo4t`|r=QHxK^&`ThU@e?}%|3K9F?208iW zme#iRW;q51d7r-i2@@yv$#?foo-ui9&&;_KW>23tcfmYgU%wUptNd5^DomT{>mD2x z6O*}PXJ)M8hDplAzK2ClCK_MYw5m7_c4R>}$h;iv9oOtl4wWrx2?Set#%9E3|E*4BR-1MNS zP0B3q!HI*0?R~N~emYDKRk~)zq?;9Fd|1@pFYnOAF;i2;bCSA-o7&BZs=QLFqSic$ zpOjAsYA&0kc6W-?Q7$HL9uA3s2M0I=bpje3OjaCSv_9c*m#p`;H8(eBTwWD>x-3wE G!5RRDKTt~m literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/select2Purple.gif b/src/main/resources/icons/generic/select2Purple.gif new file mode 100644 index 0000000000000000000000000000000000000000..6c74406cef7c8ea5a4f36a75f0168b9ce10c3705 GIT binary patch literal 701 zcmZ?wbhEHblw}ZOC}%KK{3jKhpH@vk z5GllSEglxl0$>gh~1H=SZ`I@!!*f~j$jiBYGqVVALCpNa7#Gn45S zW)n?KS`GDQI@q>(d##x}XVK(I%V$hqKY#w}*|Rn*n7?k(!i@_TY+AH%+p?wm*RI{N zWbyXp%XTbZzI)Zm%}bW-TDfBXy0tr2uH3qG$<7ta&+XoIY|oy<8#i3sxA*YIjiv;jR}LJwbK>~9BS&r@J9hunscVN0-amcn+S#*r zPoDU6=k~|jw?5pu`TpjOx7V+|xqj{4%^UCT-hF@X?)&@q9-cY#=uXov-njns!ugl??|pms_V3@n|Ni~^_4Vtg7cYMO`t{@Ur(fT{|NQy$ z>xcJWUcGwu@Zs<8-+%r5`SQt=-#@sNpO{Q3X?KO++}RS^511|hcQme#iR zW+4U!Hv7K*2@@yvv32)Oo-ui9&&;_KW>23tcfmXd2gemotDIIiuuq%o=o}In7M8wa zXE?`($;(|L(nBM+?%WZ@xp$(2t6TKWn3&LG>9Jg=`j)%K9gCL|k(8E^k(JZrzH!Dq zVP~S2sF=9Ctb(GFwhqtUTX%LQDNC45$awgOl}AD|=S~I#cZYyV@~wwAE-ZBEk+LjN zxw#^RQ<#04pQ+;H)jgfUa!GFOhF(r)4v~%p3XDz6>;jS%8&<5o&JcIFOV)eanwy(5F0YE6Z6?5A4FKge BQeFT6 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/select2Red.gif b/src/main/resources/icons/generic/select2Red.gif new file mode 100644 index 0000000000000000000000000000000000000000..4712a5e359200bf37c62508ddddb068472b351fa GIT binary patch literal 702 zcmb`5>rava9EE=ZrEX2~zUeBoiCE69<_EJcE(x{ET(g^%2AWJixmn8#iZ}_pAOTW> z2oiFey5-geqFqqSw5&{=is@>~45ubV3why%=zr+!?AduvP97!W6zwp?KsB65@WmBW z((hH%s>(767b{AuD(?oAkWZ&ql$Jq)8y5tDq+$YyhiKsZGXPU{8z22o#?Wt7mzCJ{)Ml~Am;v!+a;F&*ww!c8}AEAP9=0o{bIE;c&a%E}PBca{0Vo$J!d|@%a7zAPn|@H5g?j5u4c$ zINatW0Ae=(p+G3&6WcpFC7q9WQn^6ZrI0HXWb#=(rJtfFCqI^wDLDm&h4f(){RU}B zq|3cbzj>?3I6^N@c_|?0ox5$KQj4bOv|}^b<|`vt!!cMa&K4FCd3@&Mtf?e%A7+1W z7#@ExDmo^0Xl3MDYzTomrKDlG&5@Z)<+Qv7QTgy|9^u=Qadv!0m3>KH8jL5V#mGr> zcP^xz3=hj)nw@xyPtI3Pu_Kb>3WzFjk^H5Q;gH#SZzk-Wr~w6%ki)M>#y)nK;sQ lGnLa8m0sMFm9C#uJ*7^x9Xf9qv_31h^cTIqe~AF6{{m89J$e8D literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/selectBlue.gif b/src/main/resources/icons/generic/selectBlue.gif new file mode 100644 index 0000000000000000000000000000000000000000..ccb11b27cdc0d3016a25c9268f9583c78cee127e GIT binary patch literal 724 zcmV;_0xSJTNk%w1VIBY!0CxZ}A^sXuZ)S9NVRB_UAWdmwa&L2QW^^D=W@c$)WdI@h z1OWg5001lj00ICU01*I&00000002X7p+s+^NOi1Cd$mq~yH0<)O@6sgfxSkq4i>uP4tF)!8wW+YUsIa-Bs?iBuehqRyS2N-uC~ClyTr4+#j?A_uD8Ikxx>4}&c4Rcy2H)C$I`jM z%f7+IzsJ$Hz{(Sil&DP=8-|NrW)Pb-*Wv8m=I_?v>)PY**x~Kh;Oye*@%Z}u;OOxB`~2$f^!xk#_xbzg=hJaZ{Qc?f^Xu*J`uh9p>+AOR_Wk|+{{H^tlq{^z{G#{{aF6 z0RjU70s{d80|5d90RjU70s{d80|5d90RjU70s{d80|5d92mgQ%2?+`d5DW_ngNuxf z2@e2~2~JN?T5?-lQBO{to}ZrykpKx#QBqxUs$Ni2oS?9xl224WKwqkIU{z4Bu%5D{ zQ&ux7BVoC6tggSE!KG9|DwPE%MyHZ3I-6*pzM zW>!)8`BPI0qmz`N4Ece>g#`&0&d5QNM$H;F$>b3E=gAb6A~{-^z>q=5jy7)IKul(Y zV#O%}3A)g*;RT#HA|oC#`N<+i7$-P%@Zh0>&k7$b2%uRL z3d&RxB{h1ekinI!3LgZh!ith*36mgA(xwfvk z5CPKB3SuxYh>D5J$jY&^bMW!;n_Jl$7@I1qsM$HX1cyia_=mW9`1m*O4Q<#R*swdG zd2dkjzOeQK5giBOdJji-9!yNl$}6tS$SJCB>`0$GP(~Teft~)-{{= zZr*)l=burZ4H zC;9~PFiHn1)CRN`XmT(rMX)dig-mFa3shxfjZjVw6ldfNlwjlu)RI~ppvuI^RiVC; zQ6XS0|M~#gX^aOLC3mfBi%{ZW&EVYWzqTsk()nXymoM)Qop4?ALXv>nLs!8hPe%r8 E0Gu$1vk z5CPKB3SuxY2#NAb$cnJBv-0q9$*D*fTB>UsDcO1$nmg!thFLrNn}w#hMrV5_7Ww5i zgcrBRRQ4s-Pfcr`lRbB5{lwL!(>GO3y%0RdRop8 zc+t5z#BA}Tqm7K5w#!2gT-9`9IhXGI)H}8>IL+~hQfL2-cj+ny3Xc4Yyg|81{+x_L zf#NlR39Wu?jH-c}ER0hUk{d(~Q|C?2@Z wCqQ@_;{is2-Rt)TsB*HVbL|S)6SnY@=7o!wk0$0{znbTE&qX28(~-d%05>;%YybcN literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/source_red_cross.gif b/src/main/resources/icons/generic/source_red_cross.gif new file mode 100644 index 0000000000000000000000000000000000000000..5744ed2fbb8c5420dc810854babe0df33a9c80d4 GIT binary patch literal 434 zcmZ?wbhEHblw=TOC}%KK{3jKhpH@vk z5CPKB3SuxYure?h$jfU;NhyhmS*WVosH?{@GkbAxcrh?IXlZ%q>ql5w#oF5Dvaw|_ zFeKR9C)(R5SX)>4`_~5tXSleuM?|!Hc+Ahpn4X-xpMhaJ5LHwx&&}OmTYH>=;W`lY z_8xC)x<6^s`HqhN3=IFcx&H|X{o~{NCnNKZnfV_p>;Kx?{~aCQTUwq^pZ>vvP9U zv`_rO)GDdD*N4XU<-)4XU<-) p.exportRestrictedServiceGraphForSW(s) } + + // Export the application allocation table + p.exportAllocationTable() + + // Export the data allocation table + p.exportDataAllocationTable() + + // Export the target used by software + p.exportSWTargetUsageTable() + + // Export the routing constraints + p.exportRouteTable() + + // Export the deactivated components + p.exportDeactivatedComponents() + + // Export the transactions defined by the user + p.exportUserTransactions() + } + +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfiguration.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfiguration.scala new file mode 100644 index 0000000..ee9e474 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfiguration.scala @@ -0,0 +1,17 @@ +package pml.examples.simpleKeystone + +/** + * Transaction that are always used. + * A user transaction is considered during the analyses if identified as so. + * For instance to indicate that the t11 transaction defined in [[SimpleKeystoneTransactionLibrary]] is used + * {{{t11_app1_rd_interrupt1.used}}} + * @see [[pml.operators.Use.Ops]] for operator definition + */ +trait SimpleKeystoneLibraryConfiguration extends SimpleKeystoneTransactionLibrary with SimpleSoftwareAllocation { + self: SimpleKeystonePlatform => + + t11_app1_rd_interrupt1.used + t12_app1_rd_d1.used + t13_app1_wr_d2.used + +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationFull.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationFull.scala new file mode 100644 index 0000000..397e666 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationFull.scala @@ -0,0 +1,17 @@ +package pml.examples.simpleKeystone + +/** + * All transaction are used + */ +trait SimpleKeystoneLibraryConfigurationFull extends SimpleKeystoneLibraryConfiguration { + self: SimpleKeystonePlatform => + + t41_app4_wr_input_d.used + t211_app21_rd_input_d.used + t14_app1_rd_wr_L1.used + t212_app21_wr_d1.used + t221_app22_rd_d2.used + t222_app22_wr_output_d.used + t223_app22_st_dma_reg.used + app3_transfer.used +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationNoL1.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationNoL1.scala new file mode 100644 index 0000000..bc7f61c --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationNoL1.scala @@ -0,0 +1,16 @@ +package pml.examples.simpleKeystone + +/** + * Transactions used when the L1 is not used + */ +trait SimpleKeystoneLibraryConfigurationNoL1 extends SimpleKeystoneLibraryConfiguration { + self: SimpleKeystonePlatform => + + t41_app4_wr_input_d.used + t211_app21_rd_input_d.used + t212_app21_wr_d1.used + t221_app22_rd_d2.used + t222_app22_wr_output_d.used + t223_app22_st_dma_reg.used + app3_transfer.used +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp21.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp21.scala new file mode 100644 index 0000000..421dd9d --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp21.scala @@ -0,0 +1,13 @@ +package pml.examples.simpleKeystone + +/** + * Transaction used when app4, app1 and app21 are scheduled + */ +trait SimpleKeystoneLibraryConfigurationPlanApp21 extends SimpleKeystoneLibraryConfiguration { + self: SimpleKeystonePlatform => + + t41_app4_wr_input_d.used + t211_app21_rd_input_d.used + t212_app21_wr_d1.used + t14_app1_rd_wr_L1.used +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp22.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp22.scala new file mode 100644 index 0000000..e402a51 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneLibraryConfigurationPlanApp22.scala @@ -0,0 +1,15 @@ +package pml.examples.simpleKeystone + +/** + * Transaction used when app22, app1 and app3 are scheduled + */ +trait SimpleKeystoneLibraryConfigurationPlanApp22 extends SimpleKeystoneLibraryConfiguration { + self: SimpleKeystonePlatform => + + t14_app1_rd_wr_L1.used + t221_app22_rd_d2.used + t222_app22_wr_output_d.used + t223_app22_st_dma_reg.used + app3_transfer.used + +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystonePlatform.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystonePlatform.scala new file mode 100644 index 0000000..61ea00f --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystonePlatform.scala @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.examples.simpleKeystone + +import pml.model.hardware._ +import pml.operators._ +import sourcecode.Name + +/** + * Simple model of the Keystone platform illustrating the main features of PML. + * The components of the architectures can be created using the constructors provided in [[pml.model.hardware.BaseHardwareNodeBuilder]] + * For instance the [[package.SimpleKeystonePlatform.dma]] is built with: + * {{{val dma: Initiator = Initiator()}}} + * An axi-bus is built with: + * {{{val axi-bus: SimpleTransporter= SimpleTransporter()}}} + * A peripheral or a memory is built with + * {{{val sram: Target = Target()}}} + * Some components may be composite, if you want to define one instance of composite you can use the object + * instantiation pattern used for the TeraNet + * {{{object TeraNet extends Composite }}} + * You can also define a type of Composite that may be instantiated afterward, for instance here ARMCores are defined + * as a composition of a core (initiator) and a cache (transporter). A name is given as a parameter of the ARMCore class + * {{{class ARMCore (name:Symbol) extends Composite }}} + * Then components can be linked together, this operation simply connect the service of the same type provided by the + * two components. For instance + * {{{ARM0.core link axi_bus}}} + * links the [[pml.model.service.Load]] and [[pml.model.service.Store]] service of the ARM0.core to the ones of the axi-bus + * Beware that all links are not possible, for instance you cannot link two [[pml.model.hardware.Target]] or a + * [[pml.model.hardware.Composite]] to another component. + * @see [[pml.operators.Link.Ops]] for link operator definition + * @see [[pml.model.hardware.BaseHardwareNodeBuilder]] for component instantiation + * @param name the name of the final object merging all facets of the model + * + */ +class SimpleKeystonePlatform(name: Symbol) extends Platform(name) { + + /** + * Enable to provide the name implicitly + * @param implicitName the name of the object/class inheriting from this class + * will be the name of platform + */ + def this()(implicit implicitName: Name) = { + this(Symbol(implicitName.value)) + } + + /** Initiator modelling the DMA + * @group initiator + */ + val dma: Initiator = Initiator() + + /** Composite modelling the Teranet + * @group composite + */ + object TeraNet extends Composite { + /** Transporter modelling the peripheral interconnect + * @group transporter */ + val periph_bus: SimpleTransporter = SimpleTransporter() + + /** Transporter modelling the register interconnect + * @group transporter */ + val config_bus: SimpleTransporter = SimpleTransporter() + + periph_bus link config_bus + } + + /** Composite modelling memory subsystem + * @group composite */ + object MemorySubsystem extends Composite { + + /** Transporter modelling the MSMC controller + * @group transporter */ + val msmc: SimpleTransporter = SimpleTransporter() + + /** Transporter modelling the DDR controller + * @group transporter */ + val ddr_ctrl: SimpleTransporter = SimpleTransporter() + + /** Target modelling the SRAM peripheral + * @group transporter */ + val sram: Target = Target() + + msmc link sram + msmc link ddr_ctrl + } + + /** Composite representing Keystone ARM cores and their internal L1 cache + * @group composite_def + */ + class ARMCore(name: Symbol) extends Composite(name) { + + /** + * Enable to provide the name implicitly + * @param implicitName the name of the object/class inheriting from this class + * will be the name of composite + */ + def this()(implicit implicitName: Name) = { + this(implicitName.value) + } + + /** Initiator modelling an ARM Core + * @group initiator */ + val core: Initiator = Initiator() + + /** Transporter modelling the cache of the core + * @group target */ + val cache: Target = Target() + + // ARM access to its private L1 cache + core link cache + + } + + /* ----------------------------------------------------------- + * Global components + * ----------------------------------------------------------- */ + + /** Composite modelling ARM0 + * @group composite + */ + val ARM0 = new ARMCore() + /** + * Composite modelling ARM1 + * @group composite + */ + val ARM1 = new ARMCore() + + /** Initiator modelling ethernet peripheral + * @group initiator + */ + val eth: Initiator = Initiator() + + /** Transporter modelling AXI bus + * @group transporter + */ + val axi_bus: SimpleTransporter = SimpleTransporter() + + /** Target modelling SPI peripheral + * @group target + */ + val spi: Target = Target() + /** Target modelling MPIC peripheral + * @group target + */ + val mpic: Target = Target() + /** Target modelling SPI registers + * @group target + */ + val spi_reg: Target = Target() + /** Target modelling DMA registers + * @group target + */ + val dma_reg: Target = Target() + /** Target modelling external DDR + * @group target + */ + val ddr: Target = Target() + + /* ----------------------------------------------------------- + * Physical connections + ----------------------------------------------------------- */ + + // Each ARM core is connected to the internal interconnect + ARM0.core link axi_bus + ARM1.core link axi_bus + + ARM0.core link TeraNet.config_bus + ARM1.core link TeraNet.config_bus + + // Eth connection to internal interconnect + eth link TeraNet.periph_bus + + // Peripheral bus connections + TeraNet.periph_bus link MemorySubsystem.msmc + + TeraNet.periph_bus link spi + + // Accesses to peripherals + TeraNet.config_bus link dma_reg + TeraNet.config_bus link spi_reg + + // Accesses to config registers + axi_bus link mpic + axi_bus link MemorySubsystem.msmc + + //MSMC connections + MemorySubsystem.msmc link TeraNet.periph_bus + + MemorySubsystem.ddr_ctrl link ddr + + // DMA connections + dma link TeraNet.periph_bus + +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneTransactionLibrary.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneTransactionLibrary.scala new file mode 100644 index 0000000..5313137 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleKeystoneTransactionLibrary.scala @@ -0,0 +1,82 @@ +package pml.examples.simpleKeystone + +import pml.model.configuration.TransactionLibrary +import pml.operators._ + +import scala.language.postfixOps + +/** + * This trait contains a library of all transactions that can occur on the platform + * One way to define a [[pml.model.configuration.TransactionLibrary.Transaction]] or + * a [[pml.model.configuration.TransactionLibrary.Scenario]] is to use the read/write operators specifying + * which [[pml.model.software.Data]] is used by which [[pml.model.software.Application]]. + * For instance + * {{{val app4_wr_input_d : Transaction = Transaction(app4 write input_d)}}} + * defines a read transaction called '''app4_wr_input_d''' between the initiator of app4 and the input_d data. + * The location of the application and the data are provided in the [[SimpleSoftwareAllocation]] trait. + * + * If you want to define several paths representing a multi-transaction use the [[pml.model.configuration.TransactionLibrary.Scenario]] + * For instance + * {{{val app1_rd_wr_L1 : Scenario = Scenario(app1_rd_L1, app1_wr_L1)}}} + * defines a scenario named '''app1_rd_wr_L1''' where app1 is reading and writing L1 cache + * @note A transaction or a scenario is only '''declared''' here, it will be considered during the interference analysis if it is + * actually used. This is done in the [[SimpleKeystoneLibraryConfiguration]] files. + * A transaction should be a path from an initiator to a target, if several paths are possible a warning will be raised. + * @see [[pml.operators.Use.Ops]] for read/write operator definitions + * */ +trait SimpleKeystoneTransactionLibrary extends TransactionLibrary { + self: SimpleKeystonePlatform with SimpleSoftwareAllocation => + + /** t41: Each time an [[SimpleKeystonePlatform.eth]] frame arrives, it transfers the payload of the frame + * to [[SimpleKeystonePlatform.MemorySubsystem.sram]] + * @group transaction_def */ + val t41_app4_wr_input_d : Transaction = Transaction(app4 write input_d) + + /** t211: [[SimpleSoftwareAllocation.app21]] reads the last [[SimpleKeystonePlatform.eth]] message + * from [[SimpleKeystonePlatform.MemorySubsystem.sram]] + * @group transaction_def */ + val t211_app21_rd_input_d : Transaction = Transaction(app21 read input_d) + + /** t212: [[SimpleSoftwareAllocation.app21]] makes some input treatments on the message, and makes + * it available for [[SimpleSoftwareAllocation.app1]] in [[SimpleKeystonePlatform.ddr]] + * @group transaction_def*/ + val t212_app21_wr_d1 : Transaction = Transaction(app21 write d1) + + /** t11: [[SimpleSoftwareAllocation.app1]] begins by reading the interrupt code from [[SimpleKeystonePlatform.mpic]] + * @group transaction_def */ + val t11_app1_rd_interrupt1 : Transaction = Transaction(app1 read interrupt1) + + /** t12: [[SimpleSoftwareAllocation.app1]] reads its input data from [[SimpleKeystonePlatform.ddr]] + * @group transaction_def*/ + val t12_app1_rd_d1 : Transaction = Transaction(app1 read d1) + + /** t13: [[SimpleSoftwareAllocation.app1]] it stores its output data in [[SimpleKeystonePlatform.ddr]] + * @group transaction_def */ + val t13_app1_wr_d2 : Transaction = Transaction(app1 write d2) + + private val app1_rd_L1: Transaction = Transaction( app1 read ARM0.cache) + private val app1_wr_L1: Transaction = Transaction( app1 write ARM0.cache) + /** t14: [[SimpleSoftwareAllocation.app1]] runs using [[SimpleKeystonePlatform.ARM0]] cache + * @group scenario_def*/ + val t14_app1_rd_wr_L1 : Scenario = Scenario(app1_rd_L1, app1_wr_L1) + + /** t221: [[SimpleSoftwareAllocation.app22]] reads output data + * of [[SimpleSoftwareAllocation.app1]] from [[SimpleKeystonePlatform.ddr]] + * @group transaction_def */ + val t221_app22_rd_d2 : Transaction = Transaction(app22 read d2) + + /** t222: [[SimpleSoftwareAllocation.app22]] store the [[SimpleKeystonePlatform.spi]] frame then in [[SimpleKeystonePlatform.MemorySubsystem.sram]] + * @group transaction_def*/ + val t222_app22_wr_output_d : Transaction = Transaction(app22 write output_d) + + /** t223: [[SimpleSoftwareAllocation.app22]] wakes up the [[SimpleKeystonePlatform.dma]] by + * writing the address of the [[SimpleKeystonePlatform.spi]] + * frames into [[SimpleKeystonePlatform.dma_reg]] + * @group transaction_def*/ + val t223_app22_st_dma_reg : Transaction = Transaction(app22 write dma_reg) + + /** When woke up, [[SimpleSoftwareAllocation.app3]] reads the [[SimpleKeystonePlatform.spi]] frame + * from [[SimpleKeystonePlatform.MemorySubsystem.sram]] and transfers it to [[SimpleKeystonePlatform.spi]] + * @group scenario_def*/ + val app3_transfer: Scenario = Scenario(app3 read output_d, app3 write output_spi_frame) +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleRoutingConfiguration.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleRoutingConfiguration.scala new file mode 100644 index 0000000..4e72e97 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleRoutingConfiguration.scala @@ -0,0 +1,16 @@ +package pml.examples.simpleKeystone + +import pml.operators._ + +/** + * Routing constraints considered for simple Keystone + */ +trait SimpleRoutingConfiguration { + self: SimpleKeystonePlatform => + + //Arm cores, ethernet and dma cannot use the periph_bus from msmc + ARM0.core cannotUseLink MemorySubsystem.msmc to TeraNet.periph_bus + ARM1.core cannotUseLink MemorySubsystem.msmc to TeraNet.periph_bus + eth cannotUseLink MemorySubsystem.msmc to TeraNet.periph_bus + dma cannotUseLink MemorySubsystem.msmc to TeraNet.periph_bus +} diff --git a/src/main/scala/pml/examples/simpleKeystone/SimpleSoftwareAllocation.scala b/src/main/scala/pml/examples/simpleKeystone/SimpleSoftwareAllocation.scala new file mode 100644 index 0000000..6eb0fa2 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/SimpleSoftwareAllocation.scala @@ -0,0 +1,126 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.examples.simpleKeystone + +import pml.model.software.{Application, Data} +import pml.operators._ + +import scala.language.postfixOps + +/** + * Definition and allocation of software and data to hardware for simple Keystone. + * Declaring the application app1 + * {{{val app1: Application = Application()}}} + * Allocating app1 on [[pml.examples.simpleKeystone.SimpleKeystonePlatform.ARM0]] + * {{{app1 hostedBy ARM0.core}}} + * Declaring the data input_d + * {{{val input_d: Data = Data()}}} + * Allocating app1 on [[pml.examples.simpleKeystone.SimpleKeystonePlatform.MemorySubsystem.sram]] + * {{{input_d hostedBy MemorySubsystem.sram}}} + * @see [[pml.operators.Use.Ops]] for hostedBy operator definition + */ +trait SimpleSoftwareAllocation { + self: SimpleKeystonePlatform => + + /* ----------------------------------------------------------- + * Application declaration + * ----------------------------------------------------------- */ + + /** [[app1]] is an asynchronous applicative activated each time a external interrupt arrives + * @group application + */ + val app1: Application = Application() + + /** At each period [[app21]] reads the last Ethernet message from [[SimpleKeystonePlatform.MemorySubsystem.sram]], + * makes some input treatments on the message, and makes it available for [[app1]] in [[SimpleKeystonePlatform.ddr]]. + * @group application + */ + val app21: Application = Application() + + /** At each period [[app22]] reads output data of [[app1]] from [[SimpleKeystonePlatform.ddr]]. + * It transforms them into [[SimpleKeystonePlatform.spi]] frames. + * The frames are then store in [[SimpleKeystonePlatform.MemorySubsystem.sram]]. And finally [[app22]] wakes up [[app3]] + * by writing the address of the [[SimpleKeystonePlatform.spi]] frames into [[SimpleKeystonePlatform.dma_reg]]. + * @group application + * */ + val app22: Application = Application() + + /** [[app3]] is a microcode running on [[SimpleKeystonePlatform.dma]]. When woke up, + * [[app3]] reads the [[SimpleKeystonePlatform.spi]] frame from [[SimpleKeystonePlatform.MemorySubsystem.sram]] + * and transfers it to [[SimpleKeystonePlatform.spi]] + * @group application + * */ + val app3: Application = Application() + + /** [[app4]] is an asynchronous microcode running on the [[SimpleKeystonePlatform.eth]] component. + * Each time an [[SimpleKeystonePlatform.eth]] frame arrives, it transfers the payload of the + * frame to [[SimpleKeystonePlatform.MemorySubsystem.sram]]. + * @group application + * */ + val app4: Application = Application() + + + /* ----------------------------------------------------------- + * Data declaration + * ----------------------------------------------------------- */ + + /** Data written by [[SimpleKeystonePlatform.eth]] in [[SimpleKeystonePlatform.MemorySubsystem.sram]] and read by [[app21]] + * @group data */ + val input_d: Data = Data() + /** Data written by [[app21]] in [[SimpleKeystonePlatform.ddr]] and read by [[app1]] + * @group data */ + val d1: Data = Data() + /** Interrupt read by [[app1]] + * @group data */ + val interrupt1: Data = Data() + /** Data written by [[app1]] in [[SimpleKeystonePlatform.ddr]] and read by [[app22]] + * @group data */ + val d2: Data = Data() + /** Data written by [[app22]] in [[SimpleKeystonePlatform.MemorySubsystem.sram]] and read by [[SimpleKeystonePlatform.dma]] + * @group data*/ + val output_d: Data = Data() + /** Register value written by [[app21]] in [[SimpleKeystonePlatform.dma_reg]] + * @group data */ + val dma_red_value: Data = Data() + /** [[SimpleKeystonePlatform.spi]] frame put by [[SimpleKeystonePlatform.dma]] on the [[SimpleKeystonePlatform.spi]] port + * @group data */ + val output_spi_frame: Data = Data() + + /* ----------------------------------------------------------- + * Data allocation + * ----------------------------------------------------------- */ + + input_d hostedBy MemorySubsystem.sram + d1 hostedBy ddr + interrupt1 hostedBy mpic + d2 hostedBy ddr + output_d hostedBy MemorySubsystem.sram + dma_red_value hostedBy dma_reg + output_spi_frame hostedBy spi + + /* ----------------------------------------------------------- + * Application allocation + * ----------------------------------------------------------- */ + + app1 hostedBy ARM0.core + app21 hostedBy ARM1.core + app22 hostedBy ARM1.core + app3 hostedBy dma + app4 hostedBy eth + +} diff --git a/src/main/scala/pml/examples/simpleKeystone/package.scala b/src/main/scala/pml/examples/simpleKeystone/package.scala new file mode 100644 index 0000000..288b3b7 --- /dev/null +++ b/src/main/scala/pml/examples/simpleKeystone/package.scala @@ -0,0 +1,12 @@ +package pml.examples + +/** + * Package containing an example on a simplification of a TI Keystone platform + * @see [[SimpleKeystonePlatform]] for examples of [[pml.model.hardware.Hardware]] modelling features + * [[SimpleSoftwareAllocation]] for examples of [[pml.model.software.Application]] modelling features + * [[SimpleKeystoneTransactionLibrary]] for examples of transaction/scenario modelling features + * [[SimpleKeystoneLibraryConfiguration]] for example of transaction library configuration + * [[SimpleKeystoneLibraryConfigurationFull]] for examples of specialisation of library configurations + * [[SimpleKeystoneExport]] for examples of platform instantiation and [[pml.exporters]] usage + */ +package object simpleKeystone diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleRoutingConfiguration.scala b/src/main/scala/pml/examples/simpleT1042/SimpleRoutingConfiguration.scala new file mode 100644 index 0000000..2a196ad --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleRoutingConfiguration.scala @@ -0,0 +1,12 @@ +package pml.examples.simpleT1042 + +trait SimpleRoutingConfiguration { + self: SimpleT1042Platform => + + /** ----------------------------------------------------------- + * Routing constraints (OPTIONAL) + * Restrict the possible paths to target by encoding + * of some routing rules + * ----------------------------------------------------------- */ + +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleSoftwareAllocation.scala b/src/main/scala/pml/examples/simpleT1042/SimpleSoftwareAllocation.scala new file mode 100644 index 0000000..cd38b7f --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleSoftwareAllocation.scala @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.examples.simpleT1042 + +import pml.model.software.{Application, Data} +import pml.operators._ + +import scala.language.postfixOps + +trait SimpleSoftwareAllocation { + self: SimpleT1042Platform => + + val configName: Symbol = Symbol("WithConf") + + /** ----------------------------------------------------------- + * Configuration specification + * ----------------------------------------------------------- */ + + /** ----------------------------------------------------------- + * Application specification : + * ----------------------------------------------------------- */ + val app1: Application = Application() + val app21: Application = Application() + val app22: Application = Application() + + val app3: Application = Application() + val app4: Application = Application() + + + /** ----------------------------------------------------------- + * Data specification + * ----------------------------------------------------------- */ + // Data written by eth in mem2 and read by app21 + val input_d: Data = Data() + // Data written by app21 in mem1 and read by app1 + val d1: Data = Data() + // Interrupt read by app1 + val interrupt1: Data = Data() + // Data written by app1 in men1 and read by app21 + val d2: Data = Data() + // Data written by app21 in mem2 and read by dma + val output_d: Data = Data() + // Register value written by app21 in DMA_reg + val dma_red_value: Data = Data() + // PCIe frame put by dma on the PCIe port + val output_pcie_frame: Data = Data() + + /** ----------------------------------------------------------- + * Data allocation + * ----------------------------------------------------------- */ + + input_d hostedBy mem2 + d1 hostedBy mem1 + interrupt1 hostedBy mpic + d2 hostedBy mem1 + output_d hostedBy mem2 + dma_red_value hostedBy dma_reg + output_pcie_frame hostedBy pcie + + /** ----------------------------------------------------------- + * Application allocation + * ----------------------------------------------------------- */ + + app1 hostedBy C1.core + app21 hostedBy C2.core + app22 hostedBy C2.core + app3 hostedBy dma + app4 hostedBy eth + +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042Export.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042Export.scala new file mode 100644 index 0000000..60acd21 --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042Export.scala @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.examples.simpleT1042 + +import pml.exporters._ +import pml.operators._ +import views.interference.examples.simpleT1042.{SimpleT1042ApplicativeTableBasedInterferenceSpecification, SimpleT1042PhysicalTableBasedInterferenceSpecification} + +object SimpleT1042Export extends App { + + object SimpleT1042ConfiguredFull extends SimpleT1042Platform(Symbol("SimpleFull")) + with SimpleT1042LibraryConfigurationFull + with SimpleRoutingConfiguration + with SimpleT1042PhysicalTableBasedInterferenceSpecification + with SimpleT1042ApplicativeTableBasedInterferenceSpecification + + object SimpleT1042ConfiguredNoL1 extends SimpleT1042Platform(Symbol("SimpleNoL1")) + with SimpleT1042LibraryConfigurationNoL1 + with SimpleRoutingConfiguration + with SimpleT1042PhysicalTableBasedInterferenceSpecification + with SimpleT1042ApplicativeTableBasedInterferenceSpecification + + object SimpleT1042ConfiguredPlanApp21 extends SimpleT1042Platform(Symbol("SimplePlanApp21")) + with SimpleT1042LibraryConfigurationPlanApp21 + with SimpleRoutingConfiguration + with SimpleT1042PhysicalTableBasedInterferenceSpecification + with SimpleT1042ApplicativeTableBasedInterferenceSpecification + + object SimpleT1042ConfiguredPlanApp22 extends SimpleT1042Platform(Symbol("SimplePlanApp22")) + with SimpleT1042LibraryConfigurationPlanApp22 + with SimpleRoutingConfiguration + with SimpleT1042PhysicalTableBasedInterferenceSpecification + with SimpleT1042ApplicativeTableBasedInterferenceSpecification + + for (p <- Set(SimpleT1042ConfiguredFull,SimpleT1042ConfiguredNoL1,SimpleT1042ConfiguredPlanApp21,SimpleT1042ConfiguredPlanApp22)) { + // Export only general HW dependencies used by SW (explicit) + p.exportRestrictedHWAndSWGraph() + + // Export individually the Service graph of each software + p.applications foreach { s => p.exportRestrictedServiceGraphForSW(s) } + + p.exportAllocationTable() + p.exportDataAllocationTable() + p.exportSWTargetUsageTable() + p.exportRouteTable() + p.exportDeactivatedComponents() + p.exportUserTransactions() + } + + +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfiguration.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfiguration.scala new file mode 100644 index 0000000..3b66c7c --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfiguration.scala @@ -0,0 +1,10 @@ +package pml.examples.simpleT1042 + +trait SimpleT1042LibraryConfiguration extends SimpleT1042TransactionLibrary with SimpleSoftwareAllocation { + self: SimpleT1042Platform => + + app1_rd_interrupt1.used + app1_rd_d1.used + app1_wr_d2.used + +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationFull.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationFull.scala new file mode 100644 index 0000000..e6122cb --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationFull.scala @@ -0,0 +1,14 @@ +package pml.examples.simpleT1042 + +trait SimpleT1042LibraryConfigurationFull extends SimpleT1042LibraryConfiguration { + self: SimpleT1042Platform => + + app4_wr_input_d.used + app21_rd_input_d.used + app1_rd_wr_L1.used + app21_wr_d1.used + app22_rd_d2.used + app22_wr_output_d.used + app22_st_dma_reg.used + app3_transfer.used +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationNoL1.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationNoL1.scala new file mode 100644 index 0000000..e486ff4 --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationNoL1.scala @@ -0,0 +1,13 @@ +package pml.examples.simpleT1042 + +trait SimpleT1042LibraryConfigurationNoL1 extends SimpleT1042LibraryConfiguration { + self: SimpleT1042Platform => + + app4_wr_input_d.used + app21_rd_input_d.used + app21_wr_d1.used + app22_rd_d2.used + app22_wr_output_d.used + app22_st_dma_reg.used + app3_transfer.used +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp21.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp21.scala new file mode 100644 index 0000000..904e4c9 --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp21.scala @@ -0,0 +1,10 @@ +package pml.examples.simpleT1042 + +trait SimpleT1042LibraryConfigurationPlanApp21 extends SimpleT1042LibraryConfiguration { + self: SimpleT1042Platform => + + app4_wr_input_d.used + app21_rd_input_d.used + app21_wr_d1.used + app1_rd_wr_L1.used +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp22.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp22.scala new file mode 100644 index 0000000..1d4bef9 --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042LibraryConfigurationPlanApp22.scala @@ -0,0 +1,12 @@ +package pml.examples.simpleT1042 + +trait SimpleT1042LibraryConfigurationPlanApp22 extends SimpleT1042LibraryConfiguration { + self: SimpleT1042Platform => + + app1_rd_wr_L1.used + app22_rd_d2.used + app22_wr_output_d.used + app22_st_dma_reg.used + app3_transfer.used + +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042Platform.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042Platform.scala new file mode 100644 index 0000000..ebf0f84 --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042Platform.scala @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.examples.simpleT1042 + +import pml.model.hardware._ +import pml.operators._ +import sourcecode.Name + +class SimpleT1042Platform(name: Symbol) extends Platform(name) { + + // DMA + val dma: Initiator = Initiator() + + // 2 Cached Cores + val C1 = new CachedCore() + val C2 = new CachedCore() + // Ethernet initiator + val eth: Initiator = Initiator() + + /* ----------------------------------------------------------- + * Global components + * ----------------------------------------------------------- */ + + // Composite representing cores and their internal L1 cache + class CachedCore(name: Symbol) extends Composite(name) { + + def this()(implicit implicitName: Name) = { + this(implicitName.value) + } + + val core: Initiator = Initiator() + val L1: Target = Target() + val cpu: Target = Target() + + // ARM access to its private L1 cache + core link L1 + core link cpu + + } + + // Interconnect + val bus: SimpleTransporter = SimpleTransporter() + + // Register configuration bus + val config_bus: SimpleTransporter = SimpleTransporter() + + // Memories peripheral + val mem1: Target = Target() + val mem2: Target = Target() + + // PCIe peripheral + val pcie: Target = Target() + // MPIC peripheral + val mpic: Target = Target() + // PCIe_reg peripheral + val pcie_reg: Target = Target() + // DMA_reg peripheral + val dma_reg: Target = Target() + + /* ----------------------------------------------------------- + * Physical connections + ----------------------------------------------------------- */ + + // Each ARM core is connected to the internal interconnect + C1.core link bus + C2.core link bus + + + // Eth connection to internal interconnect + eth link bus + + // Memory connections + bus link mem1 + bus link mem2 + + // Interconnect to configuration bu + bus link config_bus + + // Accesses to peripherals + config_bus link dma_reg + config_bus link pcie_reg + config_bus link mpic + + // DMA connections + dma link pcie + dma link bus +} diff --git a/src/main/scala/pml/examples/simpleT1042/SimpleT1042TransactionLibrary.scala b/src/main/scala/pml/examples/simpleT1042/SimpleT1042TransactionLibrary.scala new file mode 100644 index 0000000..aba8f7d --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/SimpleT1042TransactionLibrary.scala @@ -0,0 +1,35 @@ +package pml.examples.simpleT1042 + +import pml.model.configuration.TransactionLibrary +import pml.operators._ + +import scala.language.postfixOps + +trait SimpleT1042TransactionLibrary extends TransactionLibrary{ + self: SimpleT1042Platform with SimpleSoftwareAllocation => + + /** ----------------------------------------------------------- + * Target usage + * ----------------------------------------------------------- */ + + val app4_wr_input_d : Transaction = Transaction(app4 write input_d) + val app21_rd_input_d : Transaction = Transaction(app21 read input_d) + val app21_wr_d1 : Transaction = Transaction(app21 write d1) + val app1_rd_interrupt1 : Transaction = Transaction(app1 read interrupt1) + val app1_rd_d1 : Transaction = Transaction(app1 read d1) + val app1_wr_d2 : Transaction = Transaction(app1 write d2) + val app1_rd_L1: Transaction = Transaction( app1 read C1.L1) + val app1_wr_L1: Transaction = Transaction( app1 write C1.L1) + val app1_rd_wr_L1 : Scenario = Scenario(app1_rd_L1, app1_wr_L1) + val app22_rd_d2 : Transaction = Transaction(app22 read d2) + val app22_wr_output_d : Transaction = Transaction(app22 write output_d) + val app22_st_dma_reg : Transaction = Transaction(app22 write dma_reg) + + +/** ----------------------------------------------------------- + * DMA copies + * ----------------------------------------------------------- */ +// val app3_rd_output_d : Transaction = Transaction(app3 read output_d) +// val app3_wr_output_pcie_frame : Transaction = Transaction(app3 write output_pcie_frame) + val app3_transfer: Scenario = Scenario(app3 read output_d, app3 write output_pcie_frame) +} diff --git a/src/main/scala/pml/examples/simpleT1042/package.scala b/src/main/scala/pml/examples/simpleT1042/package.scala new file mode 100644 index 0000000..ae573e6 --- /dev/null +++ b/src/main/scala/pml/examples/simpleT1042/package.scala @@ -0,0 +1,6 @@ +package pml.examples + +/** + * Package containing an example on a simplification of T1042 platform + * */ +package object simpleT1042 diff --git a/src/main/scala/pml/exporters/FileManager.scala b/src/main/scala/pml/exporters/FileManager.scala new file mode 100644 index 0000000..9bc95cd --- /dev/null +++ b/src/main/scala/pml/exporters/FileManager.scala @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.exporters + +import java.io.File +import scala.io.{BufferedSource, Source} +//import scala.reflect.io.Path + +object FileManager { + + /** + * Util case class encoding an output directory used by exporters and solvers + * @param name the name of the directory stored at the project location + */ + final case class OutputDirectory(name:String){ + private val directory = { + val file = new File(name) + if (file.exists()) + file + else if (file.mkdir()) { + file + } else { + throw new Exception(s"cannot create $file directory") + } + } + + /** + * Create a new file in the current directory + * @param s the file name + * @return the java File + */ + def getFile(s: String): File = { + new File(directory, s) + } + + /** + * remove all the existing files of the directory + */ + def clean(): Unit = ??? + + /** + * find recursively a file by its name + * @param name name of the file to find + * @return the java File if found + */ + def locate(name: String) : Option[File] = + OutputDirectory.recursiveLocateFirstFile(directory, (f: File) => f.getName == name) + + } + + object OutputDirectory { + def recursiveLocateFirstFile(dir: File, filter: File => Boolean): Option[File] = { + if (dir.exists()) { + if (dir.isDirectory) { + val these = dir.listFiles + (for (r <- these.find(filter)) yield { + r + }) orElse { + these.filter(_.isDirectory).flatMap(x => recursiveLocateFirstFile(x, filter)).toList match { + case Nil => None + case h :: _ => Some(h) + } + } + } else { + Some(dir).filter(filter) + } + } else + None + } + } + + val analysisDirectory: OutputDirectory = OutputDirectory("analysis") + + val exportDirectory: OutputDirectory = OutputDirectory("export") + + val temporaryDirectory: OutputDirectory = OutputDirectory("temp") + + def extractResource(name:String) : Option[BufferedSource] = { + val classLoader = getClass.getClassLoader + val resource = Source.fromInputStream(classLoader.getResourceAsStream(name)) + Option(resource) + } +} + diff --git a/src/main/scala/pml/exporters/RelationExporter.scala b/src/main/scala/pml/exporters/RelationExporter.scala new file mode 100644 index 0000000..29c1a87 --- /dev/null +++ b/src/main/scala/pml/exporters/RelationExporter.scala @@ -0,0 +1,260 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.exporters + +import pml.model.configuration.TransactionLibrary +import pml.model.hardware.Platform +import pml.model.software._ +import pml.operators._ + +import java.io.FileWriter + +object RelationExporter { + + /** + * If an element x contains relations then these relations can be exported. + * + * The routing relation can be exported as a table, the format is provided [[pml.exporters.Ops.exportRouteTable]] + * {{{ + * x.exportRouteTable() + * }}} + * The allocation of application to initiators can be exported as a table, the format is provided [[pml.exporters.Ops.exportAllocationTable]] + * {{{ + * x.exportAllocationTable() + * }}} + * The allocation of data to targets can be exported as a table, the format is provided [[pml.exporters.Ops.exportDataAllocationTable]] + * {{{ + * x.exportDataAllocationTable() + * }}} + * The service targeted by software can be exported as a table, the format is provided [[pml.exporters.Ops.exportSWTargetUsageTable]] + * {{{ + * x.exportSWTargetUsageTable() + * }}} + * The deactivated components can be exported as a table, the format is provided [[pml.exporters.Ops.exportDeactivatedComponents]] + * {{{ + * x.exportDeactivatedComponents() + * }}} + */ + trait Ops { + + /** + * Implicit class used for method extension of platform to provide + * export features + * + * @param platform the platform providing the export features + */ + implicit class Ops(platform: Platform) { + private val routingExportName: String = platform.fullName + "RouteTable.txt" + private val swAllocationExportName: String = platform.fullName + "AllocationTable.txt" + private val dataAllocationExportName: String = platform.fullName + "DataTable.txt" + private val swTargetUsage: String = platform.fullName + "TargetedServiceTable.txt" + private val componentStatus: String = platform.fullName + "ComponentStatusTable.txt" + + private def getWriter(name: String): FileWriter = { + val file = FileManager.exportDirectory.getFile(name) + new FileWriter(file) + } + + /** + * Export the table providing for each service the select next service(s) w.r.t. the initiator + * and the target service + * FORMAT: + * Initiator, TargetService, Router, NextService(s) + * (current_service_name, initiator_name, target_service_name(, next_service_name )+)+ + */ + def exportRouteTable(): Unit = { + val writer = getWriter(routingExportName) + writer.write(s"Initiator, TargetService, Router, NextService(s)\n") + platform.InitiatorRouting._values + .map(p => s"${p._1._1}, ${p._1._2}, ${p._1._3}, ${p._2.toSeq.sortBy(_.name.name).mkString(", ")}\n") + .toSeq + .sorted + .foreach(writer.write) + writer.flush() + writer.close() + } + + /** + * Export the allocation table providing for each software its initiator + * FORMAT: + * Software, Initiator(s) + * (software_name(, initiator_name )+)+ + */ + def exportAllocationTable(): Unit = { + val writer = getWriter(swAllocationExportName) + writer.write(s"Software, Initiator(s)\n") + platform.SWUseInitiator._values + .map(p => s"${p._1}, ${p._2.toSeq.sortBy(_.name.name).mkString(", ")}\n") + .toSeq + .sorted + .foreach(writer.write) + writer.flush() + writer.close() + } + + /** + * Export the allocation table providing for each data its target + * FORMAT: + * Data, Target + * (data_name, target_name)+ + */ + def exportDataAllocationTable(): Unit = { + val writer = getWriter(dataAllocationExportName) + writer.write(s"Data, Target\n") + import platform._ + for (d <- Data.all.toSeq.sortBy(_.name.name)) + writer.write(s"$d, ${d.hostingTargets.mkString(",")}\n") + writer.flush() + writer.close() + } + + /** + * Export the service requested by software + * FORMAT: + * Software, Target Service(s) + * (software_name(, service_name )+)+ + */ + def exportSWTargetUsageTable(): Unit = { + val writer = getWriter(swTargetUsage) + writer.write(s"Software, Target Service(s)\n") + platform.SWUseService._values + .map(p => s"${p._1}, ${p._2.toSeq.sortBy(_.name.name).mkString(", ")}\n") + .toSeq + .sorted + .foreach(writer.write) + writer.flush() + writer.close() + } + + /** + * Export the utilisation status of the components. + * Three possibilities, either: + * - not used and deactivated, + * - used and activated, + * - no used and activated (consider modifying the PML model in such case) + * FORMAT: + * Component, Activated, Used + * (component_name, (Yes|No), (Yes|No))+ + */ + def exportDeactivatedComponents(): Unit = { + val writer = getWriter(componentStatus) + writer.write(s"Component, Activated, Used\n") + import platform._ + val restricted = platform.hardwareGraph() + val hwLinks = restricted flatMap { p => p._2 map { x => (p._1, x) } } + val used = hwLinks.flatMap { p => Set(p._1, p._2) }.toSet + for (c <- platform.directHardware.toSeq.sortBy(_.name.name)) { + if (c.services.isEmpty) + writer.write(s"$c, No, No\n") + else if (used.contains(c)) + writer.write(s"$c, Yes, Yes\n") + else + writer.write(s"$c, Yes, No\n") + } + writer.flush() + writer.close() + } + } + + /** + * Extension export methods for configured platform + * + * @param platform the configured platforms + */ + implicit class OpsConfig(platform: Platform) { + + private def getWriter(name: String): FileWriter = { + val file = FileManager.exportDirectory.getFile(name) + new FileWriter(file) + } + + private val transactionTable: String = platform.fullName + "UsedTransactionTable.txt" + + /** + * Export the transactions used by a platform + * FORMAT: + * Transaction Name, Transaction Path + * (transaction_name, service_name(.service_name)*)+ + */ + def exportPhysicalTransactions(): Unit = { + val writer = getWriter(transactionTable) + writer.write(s"Transaction Name, Transaction Path\n") + import platform._ + for { + (n,t) <- transactionsByName.toSeq.sortBy(_.toString()) + } + writer.write(s"$n, ${t.mkString("\u2219")}\n") + writer.flush() + writer.close() + } + } + + /** + * Extension export methods for configured platform + * + * @param platform the configured platform with a library + */ + implicit class OpsLibrary(platform: Platform with TransactionLibrary) { + + private def getWriter(name: String): FileWriter = { + val file = FileManager.exportDirectory.getFile(name) + new FileWriter(file) + } + + private val transactionTable: String = platform.fullName + "UserTransactionTable.txt" + private val scenarioTable: String = platform.fullName + "UserScenarioTable.txt" + + /** + * Export the transactions used by a platform + * FORMAT: + * Transaction Name, Transaction Path + * (transaction_name, service_name(.service_name)*)+ + */ + def exportUserTransactions(): Unit = { + val writer = getWriter(transactionTable) + writer.write(s"Transaction Name, Transaction Path\n") + import platform._ + for { + (n,t) <- transactionByUserName.toSeq.sortBy(_.toString()) + } yield + writer.write(s"$n, ${transactionsByName(t).mkString("\u2219")}\n") + writer.flush() + writer.close() + } + + /** + * Export the scenarios used by a platform + * FORMAT: + * Scenario Name, Scenario Path + * (scenario_name, service_name(.service_name)*)+ + */ + def exportUserScenarios(): Unit = { + val writer = getWriter(scenarioTable) + writer.write(s"Scenario Name, Scenario Path\n") + import platform._ + for { + (n,s) <- scenarioByUserName.toSeq.sortBy(_._1.toString) + t <- s.map(transactionsByName).toSeq.sortBy(_.toString()) + } + writer.write(s"$n, ${t.mkString("\u2219")}\n") + writer.flush() + writer.close() + } + } + } +} diff --git a/src/main/scala/pml/exporters/UMLExporter.scala b/src/main/scala/pml/exporters/UMLExporter.scala new file mode 100644 index 0000000..3d7b90a --- /dev/null +++ b/src/main/scala/pml/exporters/UMLExporter.scala @@ -0,0 +1,660 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.exporters + +import pml.model.hardware._ +import pml.model.service.{ArtificialService, Service} +import pml.model.software.Application +import pml.operators._ + +import java.io.{FileWriter, Writer} +import scala.collection.mutable.{HashMap => MHashMap} + +object UMLExporter { + + /** + * Extension methods + */ + trait Ops { + + /** + * Extension methods of platform to provide + * uml export features + * @param platform the platform providing the export features + */ + implicit class UmlExporterOps(platform: Platform) { + + /** + * The name of the export file will be + * platform_nameExporter_name.exporter_extension + * @param exporter the exporter used for the platform + * @return the name of the export file + */ + def umlExportName(exporter: PlatformExporter) : String = + platform.fullName + exporter.name.name + "." + exporter.extension.name + + //TODO inconsistency with the platform naming format + /** + * For a software the name of the export file will be + * platform_nameSoftware_name.exporter_extension + * @param sw the software to export + * @param exporter the exporter used for the platform + * @return the name of the export file + */ + def umlExportName(sw:Application, exporter: RestrictedPlatformExporter) : String = + platform.fullName + sw.name.name + "." + exporter.extension.name + + /** + * Generate a writer from a file name, located in the + * export directory provided by [[FileManager]] + * @param name the file name + * @return the writer + */ + def getWriter(name:String): FileWriter = { + val file = FileManager.exportDirectory.getFile(name) + new FileWriter(file) + } + + /** + * Export the software and hardware connection graph (whether used or not) + * as a graphviz file + * @param exporter the implicit graphviz exporter available at method call + */ + def exportHWAndSWGraph()(implicit exporter: DOTRelationExporter + with FullPlatformExporter + with FullDOTHWNamer + with FullDOTSWNamer + with NullServiceNamer + with FullHWExporter + with FullSWExporter):Unit = { + val writer = getWriter(umlExportName(exporter)) + exporter.exportUML(platform)(writer) + writer.close() + } + + /** + * Export the service connection graph (whether used or not) + * as a graphviz file + * @param exporter the implicit graphviz exporter available at method call + */ + def exportServiceGraph()(implicit exporter: DOTRelationExporter + with FullPlatformExporter + with NullHWNamer + with NullSWNamer + with FullDOTServiceNamer + with FullServiceExporter + with NullHWExporter + with NullSWExporter):Unit = { + val writer = getWriter(umlExportName(exporter)) + exporter.exportUML(platform)(writer) + writer.close() + } + + /** + * Export the software and hardware connection graph used by the configuration + * as a graphviz file + * @param exporter the implicit graphviz exporter available at method call + */ + def exportRestrictedHWAndSWGraph()(implicit exporter: DOTRelationExporter + with RestrictedPlatformExporter + with FullDOTHWNamer + with FullDOTSWNamer + with NullServiceNamer + with FullHWExporter + with FullSWExporter):Unit = { + val writer = getWriter(umlExportName(exporter)) + exporter.exportUML(platform)(writer) + writer.close() + } + + /** + * Export the service connection graph (whether used or not) + * as a graphviz file + * @param exporter the implicit graphviz exporter available at method call + */ + def exportRestrictedServiceAndSWGraph()(implicit exporter: DOTRelationExporter + with RestrictedPlatformExporter + with NullHWNamer + with FullDOTSWNamer + with FullDOTServiceNamer + with FullServiceExporter + with NullHWExporter + with FullSWExporter):Unit = { + val writer = getWriter(umlExportName(exporter)) + exporter.exportUML(platform)(writer) + writer.close() + } + + /** + * Export the service connection graph used by given software + * as a graphviz file + * @param sw the software to export + * @param exporter the implicit graphviz exporter available at method call + */ + def exportRestrictedServiceGraphForSW(sw:Application)(implicit exporter: DOTRelationExporter + with RestrictedPlatformExporter + with NullHWNamer + with FullDOTSWNamer + with FullDOTServiceNamer + with FullServiceExporter + with NullHWExporter + with FullSWExporter): Unit = { + val writer = getWriter(umlExportName(sw,exporter)) + exporter.exportUMLSW(platform,sw)(writer) + writer.close() + } + } + } + + /** + * Simple string writing in an implicit writer + * @param a the string to write + * @param writer the implicit writer + */ + private def writeElement(a: String)(implicit writer: Writer): Unit = writer.write(s"$a\n") + + trait RelationExporter { + + /** + * Write a composition relation + * @param a the owner element as a string + * @param b the owner element as a string + * @param writer the implicit writet + */ + def writeComposition(a: String, b: String)(implicit writer: Writer): Unit + + /** + * Write an association relation + * @param a the left element as a string + * @param b the right element as a string + * @param name the name of the relation by default empty + * @param writer the implicit writer + */ + def writeAssociation(a: String, b: String, name: String = "")(implicit writer: Writer): Unit + + /** + * Write the header of a given export + * @param writer the implicit writer + */ + def writeHeader(implicit writer: Writer): Unit + + /** + * Write the footer of a given export + * @param writer the implicit writer + */ + def writeFooter(implicit writer: Writer): Unit + } + + trait DOTRelationExporter extends RelationExporter { + + def writeComposition(a: String, b: String)(implicit writer: Writer): Unit = + writer.write(s"$a -> $b [dir=back, arrowtail=diamond]\n") + + def writeAssociation(a: String, b: String, name: String = "")(implicit writer: Writer): Unit = + writer.write(s"$a -> $b[${if (name.nonEmpty) s"label=$name," else ""} arrowhead=none]\n") + + def writeHeader(implicit writer: Writer): Unit = + writeElement( + """digraph hierarchy { + |size="5,5" + |node[shape=record,style=filled] + |edge[arrowtail=empty] + """.stripMargin) + + def writeFooter(implicit writer: Writer): Unit = writeElement("}") + } + + trait ServiceNamer { + + protected val _memoServiceId: MHashMap[Service, String] = MHashMap.empty + + /** + * Build the id of a service if possible + * @param x the service + * @param writer the implicit writer + * @return the unique id of the service + */ + def getId(x: Service)(implicit writer: Writer): Option[String] + + /** + * Build the element declaring the service + * @param x the service + * @return the element declaration as a string + */ + def getElement(x: Service): Option[String] + } + + trait ServiceExporter { + + /** + * Empty the export caches + */ + def resetService(): Unit + + /** + * Print the export representation of a link between two services + * @param from the origin service + * @param to the destination service + * @param writer the implicit writer + */ + def exportUML(from: Service, to: Service)(implicit writer: Writer): Unit + } + + trait NullServiceNamer extends ServiceNamer { + + def getId(x: Service)(implicit writer: Writer): Option[String] = None + + def getElement(x: Service): Option[String] = None + } + + trait NullServiceExporter extends ServiceExporter { + + def resetService(): Unit = {} + + def exportUML(from: Service, to: Service)(implicit writer: Writer): Unit = {} + } + + trait FullDOTServiceNamer extends ServiceNamer { + + def getId(x: Service)(implicit writer: Writer): Some[String] = Some(_memoServiceId.getOrElseUpdate(x, { + writeElement(getElement(x).value) + x.name.name + })) + + def getElement(x: Service): Some[String] = x match { + case a:ArtificialService => Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor=green]""") + case s => Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor=green]""") + } + } + + trait FullServiceExporter extends ServiceExporter { + self: ServiceNamer with RelationExporter => + + def resetService(): Unit = _memoServiceId.clear() + + def exportUML(from: Service, to: Service)(implicit writer: Writer): Unit = { + for {f <- getId(from); t <- getId(to)} yield writeAssociation(f, t) + } + } + + trait HWNamer { + + protected val _memoHWId: MHashMap[Hardware, String] = MHashMap.empty + + /** + * Reset the internal caches + */ + def resetHW(): Unit = _memoHWId.clear() + + /** + * Build the unique id of a physical element + * + * @param x the physical element + * @param writer the implicit writer + * @param pPB the implicit relation of the provided basic services + * @return the unique id + */ + def getId(x: Hardware)(implicit + writer: Writer, + pPB: Provided[Hardware, Service]): Option[String] + + /** + * Build the element declaration of the physical element + * + * @param x the physical element + * @return the element declaration + */ + def getElement(x: Hardware): Option[String] + } + + trait HWExporter { + def exportUML(from: Hardware, to: Hardware)(implicit + writer: Writer, + pPB: Provided[Hardware, Service]): Unit + } + + trait NullHWExporter extends HWExporter { + def exportUML(from: Hardware, to: Hardware)(implicit + writer: Writer, + pPB: Provided[Hardware, Service]): Unit = {} + } + + trait NullHWNamer extends HWNamer { + def getId(x: Hardware)(implicit + writer: Writer, + pPB: Provided[Hardware, Service]): None.type = None + + def getElement(x: Hardware): None.type = None + } + + trait FullDOTHWNamer extends HWNamer { + + self: RelationExporter with ServiceNamer => + + def getId(x: Hardware)(implicit + writer: Writer, + pPB: Provided[Hardware, Service]): Some[String] = Some(_memoHWId.getOrElseUpdate(x, { + writeElement(getElement(x).value) + val id = x.name.name + for {c <- x.services; cs <- getId(c)} yield writeAssociation(id, cs) + x match { + case comp: Composite => + for (c <- comp.hardware; cs <- getId(c)) yield writeComposition(id, cs) + case _ => + } + id + })) + + def getElement(x: Hardware): Some[String] = x match { + case _: Transporter => Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor = mediumpurple1]""") + case _: Target => Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor = darkolivegreen1]""") + case _: Initiator => Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor = brown1]""") + case _ => Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor = orange]""") + } + + } + + trait FullHWExporter extends HWExporter { + self: ServiceNamer with HWNamer with RelationExporter => + + def exportUML(from: Hardware, to: Hardware)(implicit + writer: Writer, + pPB: Provided[Hardware, Service]): Unit = { + for {f <- getId(from); t <- getId(to)} yield writeAssociation(f, t) + } + } + + trait SWNamer { + + /** + * Build the unique id of a software element + * @param sw the software + * @return the unique id + */ + def getId(sw: Application): Option[String] + + /** + * Build the element declaration of the software element + * @param x the software element + * @return the element declaration + */ + def getElement(x: Application): Option[String] + } + + trait SWExporter { + + def exportUML(sw: Application)(implicit + writer: Writer, + pI: Provided[Initiator, Service], + uSI: Used[Application, Initiator], + uB: Used[Application, Service], + pPB: Provided[Hardware, Service]): Unit + } + + trait FullDOTSWNamer extends SWNamer { + + def getId(x: Application): Option[String] = Some(x.name.name) + + def getElement(x: Application): Option[String] = Some(s"""${x.name.name}[label = "{${x.name.name} : ${x.typeName.name}}", fillcolor = deepskyblue1]""") + } + + trait NullSWNamer extends SWNamer { + + def getId(sw: Application): Option[String] = None + + def getElement(x: Application): Option[String] = None + } + + trait FullSWExporter extends SWExporter { + self: ServiceNamer with HWNamer with SWNamer with RelationExporter => + + def exportUML(sw: Application)(implicit + writer: Writer, + pI: Provided[Initiator, Service], + uSI: Used[Application, Initiator], + uB: Used[Application, Service], + pPB: Provided[Hardware, Service]): Unit = { + for {s <- getElement(sw)} yield writeElement(s) + // for {c <- sw.targetService; s <- getId(sw); cs <- getId(c)} yield writeAssociation(s, cs, "use") //Activate to see target services + for {c <- sw.hostingInitiators; s <- getId(sw); cs <- getId(c)} yield writeAssociation(s, cs) + for {c <- sw.hostingInitiators; b <- c.services; s <- getId(sw); bs <- getId(b)} yield writeAssociation(s, bs) + } + } + + trait NullSWExporter extends SWExporter { + def exportUML(sw: Application)(implicit + writer: Writer, + pI: Provided[Initiator, Service], + uSI: Used[Application, Initiator], + uB: Used[Application, Service], + pPB: Provided[Hardware, Service]): Unit = {} + } + + trait PlatformNamer { + + /** + * Build the unique id of the platform + * @param x the platform + * @return the id + */ + def getId(x: Platform): Option[String] + + /** + * Build the element declaration of the platform + * @param x the platform + * @return the element declaration + */ + def getElement(x: Platform): Option[String] + } + + trait PlatformExporter { + + val name:Symbol + val extension:Symbol + + /** + * Export the platform as an UML diagram + * @param platform the platform to export + * @param writer the implicit writer + */ + def exportUML(platform: Platform)(implicit writer: Writer): Unit + } + + trait FullDOTPlatformNamer extends PlatformNamer { + + def getId(x: Platform): Option[String] = Some(x.name.name) + + def getElement(platform: Platform): Option[String] = Some(s"${getId(platform)}[label = {${platform.name.name} : ${platform.typeName.name}}, fillcolor = violet]") + } + + trait NullPlatformNamer extends PlatformNamer { + + def getId(x: Platform): Option[String] = None + + def getElement(x: Platform): Option[String] = None + } + + trait FullPlatformExporter extends PlatformExporter { + self: HWExporter + with HWNamer + with SWExporter + with SWNamer + with ServiceExporter + with PlatformNamer + with RelationExporter => + + val extension: Symbol = self match { + case _ => Symbol("dot") + } + + /** + * Export the platform with all its software, hardware and services (even the ones that are not used) + * @param platform the platform to export + * @param writer the implicit writer + */ + def exportUML(platform: Platform)(implicit writer: Writer): Unit = { + import platform._ + resetService() + resetHW() + writeHeader + for {c <- platform.applications; s <- getId(platform); cs <- getId(c)} yield writeComposition(s, cs) + for {c <- platform.directHardware; s <- getId(platform); cs <- getId(c)} yield writeComposition(s, cs) + platform.PLLinkableToPL.edges foreach { p => + p._2 foreach { + exportUML(p._1, _) + } + } + platform.applications.foreach(exportUML) + platform.ServiceLinkableToService.edges foreach { p => + p._2 foreach { + exportUML(p._1, _) + } + } + writeFooter + writer.flush() + } + } + + trait RestrictedPlatformExporter extends PlatformExporter { + self: HWExporter + with HWNamer + with SWExporter + with SWNamer + with ServiceExporter + with PlatformNamer + with RelationExporter => + + val extension: Symbol = self match { + case _ => Symbol("dot") + } + + /** + * Export the platform with only the hardware, software and services that are used + * @param platform the platform to export + * @param writer the implicit writer + */ + def exportUML(platform: Platform)(implicit writer: Writer): Unit = { + import platform._ + resetService() + resetHW() + writeHeader + val hwGraph = platform.hardwareGraph() + val hwLinks = hwGraph.keySet flatMap { k => hwGraph(k) map { x => Set(k, x) } } + val hwComponents = hwLinks.flatten + for {hw <- hwComponents; p <- getId(platform); hwName <- getId(hw)} yield writeComposition(p, hwName) + hwLinks foreach { p => exportUML(p.head, p.last) } + platform.applications foreach exportUML + val serviceGraph = platform.applications flatMap { + platform.serviceGraphOf + } + val serviceLinks = serviceGraph flatMap { p => p._2 map { x => Set(p._1, x) } } + serviceLinks foreach { p => exportUML(p.head, p.last) } + writeFooter + writer.flush() + } + + /** + * Export the hardware and services used by a given software in the platform + * @param platform the platform owing the software + * @param toPrint the software to pring + * @param writer the implicit writer + */ + def exportUMLSW(platform: Platform, toPrint: Application)(implicit writer: Writer): Unit = { + import platform._ + resetService() + resetHW() + writeHeader + for {s <- getId(platform); cs <- getId(toPrint)} yield writeComposition(s, cs) + platform.hardwareGraphOf(toPrint).filter(_._2.nonEmpty).flatMap(kv => kv._2 + kv._1).foreach(hw => { + for {s <- getId(platform); cs <- getId(hw)} yield writeComposition(s, cs) + }) + exportUML(toPrint) + platform.hardwareGraphOf(toPrint) foreach { p => p._2 foreach { + exportUML(p._1, _) + } + } + platform.serviceGraphOf(toPrint) foreach { p => p._2 foreach { + exportUML(p._1, _) + } + } + writeFooter + writer.flush() + } + } + + implicit object FullDOT extends DOTRelationExporter + with FullPlatformExporter + with FullDOTPlatformNamer + with FullDOTHWNamer + with FullDOTSWNamer + with FullDOTServiceNamer + with FullServiceExporter + with FullHWExporter + with FullSWExporter { + val name: Symbol = Symbol("Full") + } + + implicit object DOTServiceOnly extends DOTRelationExporter + with FullPlatformExporter + with NullPlatformNamer + with NullHWNamer + with NullSWNamer + with FullDOTServiceNamer + with FullServiceExporter + with NullHWExporter + with NullSWExporter { + val name: Symbol = Symbol("Service") + } + + implicit object DOTHWAndSWOnly extends DOTRelationExporter + with FullPlatformExporter + with NullPlatformNamer + with FullDOTHWNamer + with FullDOTSWNamer + with NullServiceNamer + with NullServiceExporter + with FullHWExporter + with FullSWExporter { + val name: Symbol = Symbol("HWAndSW") + } + + implicit object DOTServiceAndSWClosureOnly extends DOTRelationExporter + with RestrictedPlatformExporter + with NullPlatformNamer + with NullHWNamer + with FullDOTSWNamer + with FullDOTServiceNamer + with FullServiceExporter + with NullHWExporter + with FullSWExporter { + val name: Symbol = Symbol("RestrictedServiceAndSW") + } + + implicit object DOTHWAndSWClosureOnly extends DOTRelationExporter + with RestrictedPlatformExporter + with NullPlatformNamer + with FullDOTHWNamer + with FullDOTSWNamer + with NullServiceNamer + with NullServiceExporter + with FullHWExporter + with FullSWExporter { + val name: Symbol = Symbol("RestrictedHWAndSW") + } + +} \ No newline at end of file diff --git a/src/main/scala/pml/exporters/package.scala b/src/main/scala/pml/exporters/package.scala new file mode 100644 index 0000000..2099dbe --- /dev/null +++ b/src/main/scala/pml/exporters/package.scala @@ -0,0 +1,14 @@ +package pml + +/** + * Package containing the extension methods to export PML model as tables or Graphviz. + * + * Must be imported as following to enable export extension + * {{{import pml.exporters._}}} + * + * The available extension methods are provided in [[UMLExporter.Ops]] and [[RelationExporter.Ops]] + * + * Example of usages are provided in [[pml.examples.simpleKeystone.SimpleKeystoneExport]] + */ +package object exporters extends UMLExporter.Ops + with RelationExporter.Ops diff --git a/src/main/scala/pml/model/PMLNode.scala b/src/main/scala/pml/model/PMLNode.scala new file mode 100644 index 0000000..8f4adeb --- /dev/null +++ b/src/main/scala/pml/model/PMLNode.scala @@ -0,0 +1,47 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.model + +import sourcecode.Enclosing + +import scala.language.implicitConversions + +/** + * Base class for all PML Node + */ +abstract class PMLNode(implicit enclosing: Enclosing) { + + /** Name of the node + * + * @group identifier + * */ + val name: Symbol + + /** Name of the type of PML node + * + * @group identifier + * */ + final val typeName: Symbol = Symbol(enclosing.value.split(' ').head.split('.').last) + + /** + * Print a node only by its [[name]] + * @group printer_function + * @return string representation of a PMLNode + */ + override def toString: String = name.name +} \ No newline at end of file diff --git a/src/main/scala/pml/model/PMLNodeBuilder.scala b/src/main/scala/pml/model/PMLNodeBuilder.scala new file mode 100644 index 0000000..89ed576 --- /dev/null +++ b/src/main/scala/pml/model/PMLNodeBuilder.scala @@ -0,0 +1,39 @@ +package pml.model + +import pml.model.hardware.Composite +import pml.model.utils.Owner + +import scala.collection.mutable.{HashMap => MHashMap} + +/** + * Trait for pml node builder (usually companion object) that must + * adopt an h-consign like object handling + * @tparam T the concrete type of built object + */ +trait PMLNodeBuilder[T] { + + //TODO WARNING IF TWO PLATFORMS CONTAINS THE SAME NAMED COMPOSITE THEN MIX IN THE _memo OF THE COMPOSITES' SUBCOMPONENT + protected val _memo: MHashMap[(Symbol, Symbol), T] = MHashMap.empty + + /** + * Provide all the object of the current type created for the platform, including + * the ones created in composite components + * @group embedFunctions + * @param owner the name of the platform owning the objects + * @return set of created objects + */ + def all(implicit owner: Owner): Set[T] = { + allDirect ++ Composite.allDirect.flatMap(c => all(c.currentOwner)) + } + + /** + * Provide all the object of the current type created for the platform, without + * the ones created in composite components + * @group embedFunctions + * @param owner the name of the platform owning the objects + * @return set of created objects + */ + def allDirect(implicit owner: Owner): Set[T] = + _memo.filter(_._1._1 == owner.s).values.toSet + +} diff --git a/src/main/scala/pml/model/configuration/TransactionLibrary.scala b/src/main/scala/pml/model/configuration/TransactionLibrary.scala new file mode 100644 index 0000000..ccbc7af --- /dev/null +++ b/src/main/scala/pml/model/configuration/TransactionLibrary.scala @@ -0,0 +1,464 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.configuration + +import pml.model.configuration.TransactionLibrary.* +import pml.model.hardware.Platform +import pml.model.service.{Load, Service, Store} +import pml.model.software.Application +import pml.model.utils.{Message, Owner} +import pml.model.{PMLNode, PMLNodeBuilder} +import pml.operators.* +import sourcecode.Name +import views.interference.model.specification.InterferenceSpecification +import views.interference.model.specification.InterferenceSpecification.{PhysicalTransaction, PhysicalTransactionId} +import scala.reflect.ClassTag + +/** + * Base trait for library of transactions + */ +trait TransactionLibrary { + self: Platform => + + /** + * Map from the user defined transaction to the physical transaction id + * this map does not contain user transactions with multi-path (contained in ...) + * WARNING: this lazy variable can be called ONLY AFTER TRANSACTION/SCENARIO DEFINITION + * @group user_transaction_relation + */ + final lazy val transactionByUserName: Map[UserTransactionId, PhysicalTransactionId] = + UsedTransaction.all.flatMap(u => for{t <- u.toPhysical} yield u.userName -> t).toMap + + /** + * Map from the physical transaction id to the user defined id(s) + * It is possible that a physical transaction is linked to several (or none) user defined transactions + * WARNING: this lazy variable can be called ONLY AFTER TRANSACTION/SCENARIO DEFINITION + * @group user_transaction_relation + */ + final lazy val transactionUserName: Map[PhysicalTransactionId,Set[UserTransactionId]] = + transactionByUserName + .keySet + .groupMap(k => transactionByUserName(k))(k => k).withDefaultValue(Set.empty) + + /** + * Map from the user defined scenario to the physical transaction id + * WARNING: this lazy variable can be called ONLY AFTER TRANSACTION/SCENARIO DEFINITION + * @group user_scenario_relation + */ + final lazy val scenarioByUserName: Map[UserScenarioId, Set[PhysicalTransactionId]] = { + (transactionByUserName.keySet.map(k => UserScenarioId(k.id) -> Set(transactionByUserName(k))) ++ + UsedScenario.all.map(u => u.userName -> u.toPhysical )) + .groupMapReduce(_._1)(_._2)(_ ++ _) + } + + /** + * Map from the physical scenario id (set of transaction id) to the user defined scenario(s) + * It is possible that a scenario is linked to several (or none) user defined scenarios + * WARNING: this lazy variable can be called ONLY AFTER TRANSACTION/SCENARIO DEFINITION + * @group user_scenario_relation + */ + final lazy val scenarioUserName: Map[Set[PhysicalTransactionId], Set[UserScenarioId]] = { + val result = scenarioByUserName.keySet.groupMap(k => scenarioByUserName(k))(k => k).withDefaultValue(Set.empty) + checkLibrary(transactionUserName, result) + result + } + + /** + * Map from the used scenario and the application involved in these scenarios + * WARNING: this lazy variable can be called ONLY AFTER TRANSACTION/SCENARIO DEFINITION + * @group user_scenario_relation + */ + final lazy val scenarioSW: Map[UserScenarioId, Set[Application]] = { + transactionByUserName + (UsedTransaction.all.map(k => UserScenarioId(k.name) -> k.sw) ++ + UsedScenario.all.map(k => k.userName -> k.sw)) + .groupMapReduce(_._1)(_._2)(_ ++ _) + } + + /** + * Check the transaction and scenario libraries w.r.t. the transactions computed with the actual + * the ideal (but not requested situation) is one-to-one libraries + * definition of the platform + * @group utilFun + * @param tMap the transaction library to check + * @param sMap the scenario library to check + */ + final def checkLibrary(tMap: Map[PhysicalTransactionId,Set[UserTransactionId]], + sMap: Map[Set[PhysicalTransactionId], Set[UserScenarioId]]): Unit = { + for (k<- transactionsByName.keySet) { + if( (!tMap.contains(k) || tMap(k).isEmpty) && !sMap.keySet.flatten.contains(k)) + println(Message.transactionNoInLibraryWarning(k)) + for {s <- tMap.get(k) if s.size >= 2} yield + println(Message.transactionHasSeveralNameWarning(k, s)) + } + this match { + case i: InterferenceSpecification => + for ((s, st) <- i.purifiedScenarios if !sMap.contains(st) || sMap(st).isEmpty) { + println(Message.scenarioNotInLibraryWarning(s)) + } + } + } + + /** + * Encode node that are either transactions or scenarios + * @group scenario_class + * @param name the name of the transaction or scenario + */ + sealed abstract class ScenarioLike(val name: Symbol) extends PMLNode { + val iniTgt: () => Set[(Service, Service)] + val sw: () => Set[Application] + } + + /** + * Class encoding the used defined transactions (not already used) + * @group transaction_class + * @param userName the name of the node + * @param iniTgt a by-name value providing the origin-destination service couples of the transaction (not + * evaluated during the object initialisation) + * @param sw the application that can use this transaction + */ + final class Transaction private( + val userName: UserTransactionId, + val iniTgt: () => Set[(Service, Service)], + val sw: () => Set[Application] + ) extends ScenarioLike(userName.id) { + + /** + * Consider the transaction for the analysis + * + * @return the used transaction + */ + def used: UsedTransaction = UsedTransaction(userName, iniTgt(), sw()) + + override def toString: String = s"$userName" + } + + /** + * Builder of platform [[Transaction]] + * @group transaction_class + */ + object Transaction extends PMLNodeBuilder[Transaction] { + + /** + * A transaction can be built from an application targeting a load or a store service + * + * @param iniTgt the application/target service used + * @param name the implicit name of the transaction (deduced from val used during instantiation) + * @param t the type tag used to distinguish application loads and stores + * @tparam A the type of requests + * @return the transaction (not used for now) + */ + def apply[A: AsTransaction](iniTgt: => A)(implicit name: Name): Transaction = { + val result = TransactionParam(iniTgt) + apply(UserTransactionId(Symbol(name.value)), result._1, result._2) + } + + /** + * Main constructor of a transaction, note that transaction are memoized, so if the same name is used in the same platform + * the constructor will send back the previous definition of the transaction + * + * @param name the name of the transaction + * @param iniTgt the set of initial/target services defining the transaction + * @param sw the applications that may invoke this transaction + * @param owner the owner of the transaction (the platform) + * @return the transaction (not used for now) + */ + def apply(name: UserTransactionId, iniTgt: () => Set[(Service, Service)], sw: () => Set[Application])(implicit owner: Owner): Transaction = { + _memo.getOrElseUpdate((owner.s, name.id), new Transaction(name, iniTgt, sw)) + } + + /** + * A transaction can be from an application targeting a load or a store service + * + * @param name explicit name of the transaction + * @param iniTgt the application/target service used + * @param t the type tag used to distinguish application loads and stores + * @tparam A the type of requests + * @return the transaction (not used for now) + */ + def apply[A:AsTransaction](name: String, iniTgt: => A): Transaction = { + val result = TransactionParam(iniTgt) + apply(UserTransactionId(name), result._1, result._2) + } + + /** + * A transaction can be build from another transaction + * + * @param from the initial transaction + * @param name the implicit name of the transaction (deduced from val used during instantiation) + * @return the transaction (not used for now) + */ + def apply(from: Transaction)(implicit name: Name): Transaction = + apply(UserTransactionId(Symbol(name.value)), from.iniTgt, from.sw) + } + + /** + * Class encoding the defined transactions (not already used) + * @group scenario_class + * @param userName the name of the node + * @param iniTgt a by-name value providing the origin-destination service couples of the scenario (not + * evaluated during the object initialisation) + * @param sw the application that can use this scenario + */ + final class Scenario private( + val userName: UserScenarioId, + val iniTgt: () => Set[(Service, Service)], + val sw: () => Set[Application]) extends ScenarioLike(userName.id) { + + /** + * Consider the transaction for the analysis + * + * @return the used scenario class + */ + def used: UsedScenario = UsedScenario(userName, iniTgt(), sw()) + } + + /** + * Builder of platform [[Scenario]] + * @group scenario_class + */ + object Scenario extends PMLNodeBuilder[Scenario] { + + /** + * Build scenario from two write/read based transactions + * @param iniTgtL the set of initiator/target of left member + * @param iniTgtR the set of initiator/target of right member + * @param name the implicitly derived name + * @param tA the type tag of left member to solve erasure issue + * @param tB the type tag of right member to solve erasure issue + * @tparam A the type of left request + * @tparam B the type of right request + * @return the corresponding scenario + */ + def apply[A,B](iniTgtL: => Set[A], iniTgtR: => Set[B])(using name: Name, ta: AsTransaction[Set[A]], tb:AsTransaction[Set[B]]): Scenario = { + val resultL = TransactionParam(iniTgtL) + val resultR = TransactionParam(iniTgtR) + apply(UserScenarioId( + Symbol(name.value)), + () => { + resultL._1() ++ resultR._1() + }, + () => { + resultL._2() ++ resultR._2() + }) + } + + /** + * Build a scenario from a transaction or another scenario + * @param tr the original scenario like + * @param name the implicitly derived name + * @param t the type tag to solve erasure issue + * @tparam A the type of request + * @return the resulting scenario + */ + def apply(tr: ScenarioLike)(implicit name: Name): Scenario = + apply( + UserScenarioId(Symbol(name.value)), + tr.iniTgt, + tr.sw) + + /** + * Build a Scenario from a bunch of transactions, this should not be used with anonymous transaction + * + * @param tr the set of transactions + * @param name the implicit name of the scenario (same as the variable used to refer to it) + * @param t the type tag to distinguish the type of target service + * @tparam A the type of targeted service + * @return a scenario + */ + def apply(tr: Transaction*)(implicit name: Name): Scenario = + apply( + UserScenarioId(Symbol(name.value)), + () => { + tr.flatMap(_.iniTgt()).toSet + }, + () => { + tr.flatMap(_.sw()).toSet + } + ) + + /** + * Main constructor of a scenario, note that scenarios are memoized, so if the same name is used in the same platform + * the constructor will send back the previous definition of the scenario + * + * @param name the name of the scenario + * @param iniTgt the set of initial/target services defining the scenario + * @param sw the applications that may invoke this scenario + * @param owner the owner of the transaction (the platform) + * @return the transaction (not used for now) + */ + def apply(name: UserScenarioId, iniTgt: () => Set[(Service, Service)], sw: () => Set[Application])(implicit owner: Owner): Scenario = { + _memo.getOrElseUpdate((owner.s, name.id), new Scenario(name, iniTgt, sw)) + } + } + + /** + * Class encoding the user defined scenarios used in the configuration + * @group scenario_class + * @param userName the name of the node + * @param iniTgt the origin-destination services couples + * @param sw the application that can use this scenario + */ + final class UsedScenario private( + val userName: UserScenarioId, + iniTgt: Set[(Service, Service)], + val sw: Set[Application] + ) extends PMLNode { + + val name:Symbol = userName.id + /** + * Try to find a physical transaction of the scenario + * + * @return the set of physical transaction if possible + */ + def toPhysical: Set[PhysicalTransactionId] = { + iniTgt.flatMap(it => + transactionsByName.filter(p => p._2.size >= 2 && it._1 == p._2.head && it._2 == p._2.last).toList match { + case Nil => + println(Message.impossibleScenarioWarning(userName)) + None + case (k, _) :: Nil => Some(k) + case h :: t => + println(Message.multiPathScenarioWarning(userName, h +: t)) + (h +: t).map(_._1) + } + ) + } + } + + /** + * Builder of platform [[UsedScenario]] + * @group scenario_class + */ + object UsedScenario extends PMLNodeBuilder[UsedScenario] { + + /** + * Build a used scenario from its attributes + * @param name the explicit name + * @param iniTgt the initiator/target couples + * @param sw the application that can use it + * @param owner the implicitly derived owner of the scenario + * @return the corresponding scenario (used in the interference analysis) + */ + def apply(name: UserScenarioId, iniTgt: Set[(Service, Service)], sw: Set[Application])(implicit owner: Owner): UsedScenario = { + _memo.getOrElseUpdate((owner.s, name.id), new UsedScenario(name, iniTgt, sw)) + } + } + + /** + * Class encoding the user defined transactions used in the configuration + * @group transaction_class + * @param userName the name of the node + * @param iniTgt the origin-destination services couples + * @param sw the application that can use this transaction + */ + final class UsedTransaction private( + val userName: UserTransactionId, + iniTgt: Iterable[(Service, Service)], + val sw: Set[Application] + ) extends PMLNode { + + val name:Symbol = userName.id + + /** + * Try to find a physical transaction to the user transaction + * + * @return the physical transaction if possible + */ + def toPhysical: Option[PhysicalTransactionId] = transactionsByName + .filter(p => p._2.size >= 2 && iniTgt.exists(it => it._1 == p._2.head && it._2 == p._2.last)).toList match { + case Nil => + println(Message.impossibleTransactionWarning(userName)) + None + case (k, _) :: Nil => Some(k) + case h :: t => + println(Message.multiPathTransactionWarning(userName, h +: t)) + Scenario(UserScenarioId(userName.id), () => iniTgt.toSet, () => sw).used + None + } + } + + + /** + * Builder of platform [[UsedTransaction]] + * @group transaction_class + */ + object UsedTransaction extends PMLNodeBuilder[UsedTransaction] { + + /** + * Main constructor of a used transaction, note that used transaction are memoized, so if the same name + * is used in the same platform the constructor will send back the previous definition of the used transaction + * + * @param name the name of the used transaction + * @param iniTgt the set of inital/target services defining the transaction + * @param sw the application that may invoke this transaction + * @param owner the platform owning the transaction + * @return the used transaction + */ + def apply(name: UserTransactionId, iniTgt: Iterable[(Service, Service)], sw: Set[Application])(implicit owner: Owner): UsedTransaction = { + _memo.getOrElseUpdate((owner.s, name.id), new UsedTransaction(name, iniTgt, sw)) + } + + } + + /** + * Transaction extension method + * @group transaction_operation + * @param x id of the user transaction + */ + final implicit class UserTransactionOps(x: UserTransactionId) extends TransactionLikeOps { + def paths: Set[PhysicalTransaction] = + (for { + id <- transactionByUserName.get(x) + } yield + Set(transactionsByName(id))) getOrElse Set.empty + } + + /** + * Scenario extension method + * @group scenario_operation + * @param x id of the user scenario + */ + final implicit class ScenarioOps(x: UserScenarioId) extends TransactionLikeOps { + def paths: Set[PhysicalTransaction] = + scenarioByUserName(x).flatMap(_.paths) + } +} + +object TransactionLibrary{ + + /** + * Base trait for user ids + */ + sealed abstract class UserId { + val id:Symbol + override def toString: String = id.name + } + + /** + * User id for transactions + * @param id name of the transaction + */ + final case class UserTransactionId(id:Symbol) extends UserId + + /** + * User id of scenarios + * @param id name of the scenario + */ + final case class UserScenarioId(id:Symbol) extends UserId +} diff --git a/src/main/scala/pml/model/configuration/package.scala b/src/main/scala/pml/model/configuration/package.scala new file mode 100644 index 0000000..cefffd8 --- /dev/null +++ b/src/main/scala/pml/model/configuration/package.scala @@ -0,0 +1,13 @@ +package pml.model + +/** + * Package containing configuration PML facet of a model + * + * Use [[TransactionLibrary]] to define transactions or scenarios and select which ones should be + * consider for the analysis. + * + * Example of definition are provided in [[pml.examples.simpleKeystone.SimpleKeystoneTransactionLibrary]] + * + * Example of selection are provided in [[pml.examples.simpleKeystone.SimpleKeystoneLibraryConfigurationFull]] + */ +package object configuration diff --git a/src/main/scala/pml/model/hardware/BaseHardwareNodeBuilder.scala b/src/main/scala/pml/model/hardware/BaseHardwareNodeBuilder.scala new file mode 100644 index 0000000..87da9c9 --- /dev/null +++ b/src/main/scala/pml/model/hardware/BaseHardwareNodeBuilder.scala @@ -0,0 +1,118 @@ +package pml.model.hardware + +import pml.model.PMLNodeBuilder +import pml.model.relations.ProvideRelation +import pml.model.service.{Load, Service, Store} +import pml.model.utils.Owner +import sourcecode.Name + +/** + * Base trait for all hardware node builder + * the name of the transporter is implicitly derived from the name of the variable used during instantiation. + * Usually an hardware can be constructed without arguments, where T + * can be [[Initiator]], [[SimpleTransporter]], [[Virtualizer]], [[Target]] + * {{{ + * val myHardware = T() + * }}} + * + * It is also possible to give a specific name, for instance when creating the component in a loop then the following + * constructor can bee used + * {{{ + * val hardwareSeq = for { i <- O to N } yield T(s"myHardware\$i") + * }}} + * + * It is also possible to add specific services, by default each hardware has a [[pml.model.service.Load]] + * and a [[pml.model.service.Store]] service. + * {{{ + * val myLoadService = Load() + * val myOtherLoadService = Load() + * val myStoreService = Store() + * val myHardware = T(Set(myLoadService,myOtherLoadService,myStoreService))) + * }}} + * + * It is also possible to provide the name and the services for instance + * {{{ + * val hardwareSeq = for { i <- O to N } yield T(s"myHardware\$i", Set(Load(s"myLoad\$i"), Store(s"myStore\$i")) + * }}} + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + * @tparam T the concrete type of built object + * @group builder + */ +trait BaseHardwareNodeBuilder[T <: Hardware] extends PMLNodeBuilder[T] { + + /** + * Formatting of object name + * @param name the name of the object + * @param owner the name of its owner + * @return the formatted name + * @note this method should not be used in models + * @group utilFun + */ + final def formatName(name: Symbol, owner: Owner): Symbol = Symbol(owner.s.name + "_" + name.name) + + /** + * The builder that must be implemented by specific builder + * @param name the name of the object + * @return the object + * @note this method is implemented by concrete members (e.g. [[SimpleTransporter]], no further extension should be provided + */ + protected def builder(name: Symbol): T + + + /** + * A physical component can be defined only with the basic services it provides + * The name will be retrieved by using the implicit declaration context + * (the name of the value enclosing the object) + * @example {{{ + * val mySimpleTransporter = SimpleTransporter() + * }}} + * @param basics the set of basic services provided, if empty a default store and load services are added + * @param implicitName implicitly retrieved name from the declaration context + * @param p implicitly retrieved relation linking components to their provided services + * @param owner implicitly retrieved name of the platform + * @return the physical component + * @group publicConstructor + */ + def apply(basics: Set[Service] = Set.empty)(implicit implicitName: Name, + p: ProvideRelation[Hardware, Service], + owner: Owner): T = + apply(Symbol(implicitName.value), basics) + + /** + * A physical component can be defined by its name and the basic services it provides + * A transporter is only defined by its name, so if the transporter already exists it will + * simply add the services provided by basics + * + * @param name the physical component name + * @param basics the set of basic services provided, if empty a default store and load services are added + * @param p implicitly retrieved relation linking components to their provided services + * @param owner implicitly retrieved name of the platform + * @return the physical component + * @group publicConstructor + */ + def apply(name: Symbol, basics: Set[Service])(implicit + p: ProvideRelation[Hardware, Service], + owner: Owner): T = { + val formattedName = formatName(name, owner) + val result = _memo.getOrElseUpdate((owner.s, formattedName), builder(formattedName)) + val mutableBasic = collection.mutable.Set(basics.toSeq: _*) + if (!basics.exists(_.isInstanceOf[Load])) + mutableBasic += Load(Symbol(s"${formattedName.name}_load")) + if (!basics.exists(_.isInstanceOf[Store])) + mutableBasic += Store(Symbol(s"${formattedName.name}_store")) + p.add(result, mutableBasic) + result + } + + /** + * A physical component can be defined only its name, the services will be defined by default + * @group publicConstructor + * @param name the physical component name + * @param p implicitly retrieved relation linking components to their provided services + * @param owner implicitly retrieved name of the platform + * @return the physical component + */ + def apply(name: Symbol)(implicit p: ProvideRelation[Hardware, Service], + owner: Owner): T = + apply(name, Set.empty) +} diff --git a/src/main/scala/pml/model/hardware/Composite.scala b/src/main/scala/pml/model/hardware/Composite.scala new file mode 100644 index 0000000..908ab54 --- /dev/null +++ b/src/main/scala/pml/model/hardware/Composite.scala @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +import pml.model.PMLNodeBuilder +import pml.model.hardware.Composite.formatName +import pml.model.utils.Owner +import sourcecode.Name + +/** + * Base class of sub-systems containing themselves hardware components + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + * @param n the name of the node + * @param _owner the id of the owner of the composite (the platform or another composite) + * @group hierarchical_class + */ +abstract class Composite(n: Symbol, _owner:Owner) extends Hardware { + + /** + * the id of the owner of the composite (the platform or another composite) + * @group identifier + */ + val owner: Owner = _owner + + val name: Symbol = formatName(n, owner) + + /** + * the current owner id becomes the id of the current node and + * override the previous definition of [[currentOwner]] + * @note the initial value of the owner is stored in [[owner]] + * @group identifier + */ + implicit val currentOwner: Owner = Owner(formatName(n, owner)) + + /** + * notify the initialisation of a new composite to the companion object + */ + Composite.add(this, owner) + + /** + * Provide all the physical elements declared inside the composite + * @group component_access + * @return set of declared component + */ + def hardware: Set[Hardware] = + Initiator.allDirect ++ Target.allDirect ++ Virtualizer.allDirect ++ SimpleTransporter.allDirect ++ Composite.allDirect + + /** + * Provide all the physical elements declared inside the composite and its components + * @group component_access + * @return set of all sub-components + */ + def allHardware: Set[Hardware] = hardware flatMap { + case c: Composite => c.allHardware + case o => Set(o) + } + + /** + * Alternative constructor without implicit owner + * @param name the name of the composite + * @param dummy dummy argument to avoid method signature conflict + * @param owner the implicit owner + */ + def this(name: Symbol, dummy: Int = 0)(implicit owner: Owner) = { + this(name, owner) + } + + /** + * Alternative constructor without name, nor owner + * @param implicitName the implicit name provided by the enclosing object + * @param owner the implicit owner + */ + def this()(implicit implicitName: Name, owner: Owner) = { + this(Symbol(implicitName.value), owner) + } +} + +/** + * Static methods of Composite + * @group utilFun + */ +object Composite extends PMLNodeBuilder[Composite] { + + /** + * Reuse same formatting rule as [[BaseHardwareNodeBuilder]] + * @param name the name of the composite + * @param owner the name of its owner + * @return the formatted name + */ + private def formatName(name:Symbol, owner: Owner): Symbol = Symbol(owner.s.name+"_"+name.name) + + /** + * Notify that a new composite has been defined + * @param c the composite + * @param owner its owner + */ + private def add(c: Composite, owner: Owner): Unit = _memo.addOne((owner.s, c.name), c) +} diff --git a/src/main/scala/pml/model/hardware/Hardware.scala b/src/main/scala/pml/model/hardware/Hardware.scala new file mode 100644 index 0000000..f930555 --- /dev/null +++ b/src/main/scala/pml/model/hardware/Hardware.scala @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +import pml.model.PMLNode +import sourcecode.Enclosing + +/** + * Base class for all physical element of a platform + * + * @note this class should not be specialised in user models + * @param enclosing the implicit context that can be + * used to find the source code location of the node definition + * @group hardware_class + */ +abstract class Hardware private[hardware](implicit enclosing: Enclosing) extends PMLNode diff --git a/src/main/scala/pml/model/hardware/Initiator.scala b/src/main/scala/pml/model/hardware/Initiator.scala new file mode 100644 index 0000000..c461a0b --- /dev/null +++ b/src/main/scala/pml/model/hardware/Initiator.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +/** + * Class for the smart initiator, i.e. that can initiate transactions + * @see the possible constructors are provided by [[BaseHardwareNodeBuilder]] + * @param name the name of the node + * @group initiator_class + */ +final class Initiator private(val name: Symbol) extends Hardware + +/** + * Builder of initiators + * @group builder + */ +object Initiator extends BaseHardwareNodeBuilder[Initiator] { + + /** + * Direct builder from initiator name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): Initiator = new Initiator(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/hardware/Platform.scala b/src/main/scala/pml/model/hardware/Platform.scala new file mode 100644 index 0000000..f86fcf1 --- /dev/null +++ b/src/main/scala/pml/model/hardware/Platform.scala @@ -0,0 +1,208 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.model.hardware + +import pml.model._ +import pml.model.relations.Relation +import pml.model.service.{Load, Store} +import pml.model.software.Application +import pml.model.utils.Owner +import pml.operators._ +import sourcecode.File +import views.interference.model.specification.InterferenceSpecification.{PhysicalScenario, PhysicalScenarioId, PhysicalTransaction, PhysicalTransactionId} + +import scala.collection.mutable.{HashMap => MHashMap} +import scala.language.implicitConversions + +/** + * Base class for a platform + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + * @param name the name of the node + * @param _sourceFile the implicit descriptor of the source file where the platform is defined + * @group hierarchical_class + */ +abstract class Platform(val name: Symbol)(implicit _sourceFile: File) extends PMLNode with Relation.Instances { + + /** + * the implicit descriptor of the source file where the platform is defined + * @group identifier + */ + implicit val sourceFile: File = _sourceFile + + implicit def toSymbol(s: String): Symbol = Symbol(s) + + /** + * the current owner id becomes the id of the current node + * @group identifier + */ + implicit val currentOwner: Owner = Owner(name) + + /** + * notify the initialisation of a new composite to the companion object + */ + Platform._memo(name) = this + + /** + * The full name of a platform is its base name concatenated with the configuration if available + * @group identifier + */ + final lazy val fullName: String = currentOwner.s.name + + /** + * Map from the physical transaction id and their service sequence representation + * computed through an analysis of the platform + * WARNING: this lazy variable MUST NOT be called during platform object initialisation + * @group transaction + */ + final lazy val transactionsByName: Map[PhysicalTransactionId, PhysicalTransaction] = + this.usedTransactions.groupMapReduce(transactionId)(t => t)((l, _) => l) + + /** + * Set of physical transactions + * WARNING: this lazy variable MUST NOT be called during platform object initialisation + * @group transaction + */ + final lazy val transactions: Set[PhysicalTransactionId] = transactionsByName.keySet + + /** + * Map from the sw to the physical transaction id (default is emptySet) + * WARNING: this lazy variable MUST NOT be called during platform object initialisation + * @group transaction + */ + final lazy val transactionsBySW: Map[Application, Set[PhysicalTransactionId]] = + Application.all.groupMapReduce(a => a)(a => { + val targetServices = a.targetService + val initServices = a.hostingInitiators.flatMap(_.services) + transactionsByName.collect({ + case (id, path) if targetServices.contains(path.last) && initServices.contains(path.head) => id + }).toSet + })(_ ++ _) + + + private val _transactionId = collection.mutable.HashMap.empty[PhysicalTransaction, PhysicalTransactionId] + private val _scenarioId = collection.mutable.HashMap.empty[PhysicalScenario, PhysicalScenarioId] + + /** + * Build the transaction id as "head_last_i" where i is the number of path with the same + * origin and destination as the one on build (possible when multiple paths in the architecture) + * + * @param t the sequence of services + * @return the unique transaction id + */ + protected final def transactionId(t: PhysicalTransaction): PhysicalTransactionId = _transactionId.getOrElseUpdate(t, { + val sameHT = _transactionId.keys.count(tp => t.head == tp.head && t.last == tp.last) + PhysicalTransactionId(Symbol(s"${t.head}_${t.last}_$sameHT")) + }) + + /** + * Map from the service sequence representation to their id + * WARNING: this lazy variable MUST NOT be called during platform object initialisation + * @group transaction + */ + final lazy val transactionsName: Map[PhysicalTransaction, PhysicalTransactionId] = + transactionsByName.groupMapReduce(_._2)(_._1)((l, _) => l) + + /** + * Build the scenario id as "t_1|...|t_n" + * + * @param s the set of physical transactions forming the scenario + * @return the unique id of the scenario + */ + final def scenarioId(s: PhysicalScenario): PhysicalScenarioId = _scenarioId.getOrElseUpdate(s, { + PhysicalScenarioId(Symbol(s.map(t => t.id.name).toArray.sorted.mkString("|"))) + }) + + /** + * Extension methods for transactions + */ + protected trait TransactionLikeOps { + + /** + * Check if the transaction contains only one service + * @return true if only one service + */ + def noSingletonPaths: Boolean = paths.forall(_.size > 1) + + /** + * Method that should be provided by sub-classes to access to the path + * @return the set of service paths + */ + def paths: Set[PhysicalTransaction] + + /** + * Check is the target is in the possible targets of the transaction + * @param x target to find + * @return true if the target is contained + */ + def targetIs(x: Target): Boolean = target.contains(x) + + /** + * Provide the targets of the transaction + * @return the set of targets + */ + def target: Set[Target] = paths.filter(_.size >= 2).flatMap(t => t.last.targetOwner) + + /** + * Provide the initiators fo a transaction + * @return the set of initiators + */ + def initiator: Set[Initiator] = paths.filter(_.nonEmpty).flatMap(t => t.head.initiatorOwner) + + /** + * Check is the initiator is in the possible initiators of the transaction + * @param x initiator to find + * @return true if the initiator is contained + */ + def initiatorIs(x: Initiator): Boolean = initiator.contains(x) + + /** + * Check if the transaction is a load transaction + * @return true if target services are loads + */ + def isLoad: Boolean = paths.forall(path => path.nonEmpty && path.head.isInstanceOf[Load]) + + /** + * Check if the transaction is a store transaction + * @return true if target services are stores + */ + def isStore: Boolean = paths.forall(path => path.nonEmpty && path.head.isInstanceOf[Store]) + } + + /** + * Extension methods for physical transaction identifiers + * @param x the name of the physical transaction + */ + final implicit class PhysicalTransactionOps(x: PhysicalTransactionId) extends TransactionLikeOps { + def paths: Set[PhysicalTransaction] = Set(transactionsByName(x)) + } +} + +/** + * Static methods of Platform + * @group utilFun + */ +object Platform { + + private val _memo: MHashMap[Symbol, Platform] = MHashMap.empty + + /** + * Provide all the platforms defined in the project + * @return the set of platforms + */ + def all: Set[Platform] = _memo.values.toSet +} diff --git a/src/main/scala/pml/model/hardware/SimpleTransporter.scala b/src/main/scala/pml/model/hardware/SimpleTransporter.scala new file mode 100644 index 0000000..47b966a --- /dev/null +++ b/src/main/scala/pml/model/hardware/SimpleTransporter.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +/** + * Class modelling simple transporters. + * @see the possible constructors are provided by [[BaseHardwareNodeBuilder]] + * @param name the name of the node + * @group transporter_class + */ +final class SimpleTransporter private(val name: Symbol) extends Transporter + +/** + * Builder of simple transporters + * @group builder + */ +object SimpleTransporter extends BaseHardwareNodeBuilder[SimpleTransporter] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): SimpleTransporter = new SimpleTransporter(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/hardware/Target.scala b/src/main/scala/pml/model/hardware/Target.scala new file mode 100644 index 0000000..7d4b552 --- /dev/null +++ b/src/main/scala/pml/model/hardware/Target.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +/** + * Class for all transaction destination + * @see the possible constructors are provided by [[BaseHardwareNodeBuilder]] + * @param name the name of the node + * @group target_class + */ +final class Target private(val name: Symbol) extends Hardware + +/** + * Builder of targets + * @group builder + */ +object Target extends BaseHardwareNodeBuilder[Target] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): Target = new Target(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/hardware/Transporter.scala b/src/main/scala/pml/model/hardware/Transporter.scala new file mode 100644 index 0000000..1e35972 --- /dev/null +++ b/src/main/scala/pml/model/hardware/Transporter.scala @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +import sourcecode.Enclosing + +/** + * Base class for all physical element of a platform that cannot initiate nor be the destination + * of any transaction + * @group transporter_class + * @note this class should not be specialised in user models + * @param name the name of the node + * @param enclosing the implicit context that can be + * used to find the source code location of the node definition + */ +abstract class Transporter private[hardware](implicit enclosing: Enclosing) extends Hardware diff --git a/src/main/scala/pml/model/hardware/Virtualizer.scala b/src/main/scala/pml/model/hardware/Virtualizer.scala new file mode 100644 index 0000000..fefa123 --- /dev/null +++ b/src/main/scala/pml/model/hardware/Virtualizer.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.hardware + +/** + * A virtualizer is able to control the incoming transactions + * @group transporter_class + * @see the possible constructors are provided by [[BaseHardwareNodeBuilder]] + * @param name the name of the node + */ +final class Virtualizer private(val name: Symbol) extends Transporter + +/** + * Builder of targets + * @group builder + */ +object Virtualizer extends BaseHardwareNodeBuilder[Virtualizer] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): Virtualizer = new Virtualizer(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/hardware/package.scala b/src/main/scala/pml/model/hardware/package.scala new file mode 100644 index 0000000..6ed82ab --- /dev/null +++ b/src/main/scala/pml/model/hardware/package.scala @@ -0,0 +1,9 @@ +package pml.model + +/** + * Package containing all hardware PML nodes + * @note [[SimpleTransporter]], [[Virtualizer]], [[Target]] and [[Initiator]] can only be instantiated + * in a [[Platform]] or a [[Composite]] but [[Platform]] and [[Composite]] can be specialised + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + */ +package object hardware diff --git a/src/main/scala/pml/model/package.scala b/src/main/scala/pml/model/package.scala new file mode 100644 index 0000000..607b794 --- /dev/null +++ b/src/main/scala/pml/model/package.scala @@ -0,0 +1,10 @@ +package pml + +/** + * Package containing PML nodes. + * @see [[pml.model.hardware]] for hardware nodes + * @see [[pml.model.software]] for software nodes + * @see [[pml.model.service]] for service nodes + * @see [[pml.model.configuration]] for transaction library + */ +package object model diff --git a/src/main/scala/pml/model/relations/AntiReflexiveSymmetricEndomorphism.scala b/src/main/scala/pml/model/relations/AntiReflexiveSymmetricEndomorphism.scala new file mode 100644 index 0000000..04b2af6 --- /dev/null +++ b/src/main/scala/pml/model/relations/AntiReflexiveSymmetricEndomorphism.scala @@ -0,0 +1,17 @@ +package pml.model.relations + +import pml.model.utils.Message +import sourcecode.Name + +abstract class AntiReflexiveSymmetricEndomorphism[A](iniValues: Map[A, Set[A]])(using n:Name) extends Endomorphism[A](iniValues) { + override def add(a: A, b: A): Unit = if(a != b){ + super.add(a, b) + super.add(b, a) + } else + println(Message.errorAntiReflexivityViolation(a,name)) + + override def remove(a: A, b: A): Unit = { + super.remove(a, b) + super.remove(b, a) + } +} diff --git a/src/main/scala/pml/model/relations/AuthorizeRelation.scala b/src/main/scala/pml/model/relations/AuthorizeRelation.scala new file mode 100644 index 0000000..ddcbbae --- /dev/null +++ b/src/main/scala/pml/model/relations/AuthorizeRelation.scala @@ -0,0 +1,28 @@ +package pml.model.relations + +import pml.model.service.Service +import pml.model.software.Application + +/** + * The relations used to encode authorized requests + * + * @param iniValues initial values of the relation + * @tparam L the left type + * @tparam R the right type + */ +case class AuthorizeRelation[L, R] private(iniValues: Map[L, Set[R]]) extends Relation[L, R](iniValues) + +object AuthorizeRelation { + /** + * The instances for the authorize relation + */ + trait Instances { + + /** + * [[pml.model.service.Service]] that can be used by a [[pml.model.software.Application]] + * @group auth_relation + */ + final implicit val SWAuthorizeService: AuthorizeRelation[Application, Service] = AuthorizeRelation(Map.empty) + + } +} diff --git a/src/main/scala/pml/model/relations/Endomorphism.scala b/src/main/scala/pml/model/relations/Endomorphism.scala new file mode 100644 index 0000000..4302a2b --- /dev/null +++ b/src/main/scala/pml/model/relations/Endomorphism.scala @@ -0,0 +1,57 @@ +package pml.model.relations + +import scalaz.Memo.immutableHashMapMemo +import sourcecode.Name + +/** + * Refinement for endomorphisms (relation on the same type A) + * + * @param iniValues initial values of the relation + * @tparam A the elements type + */ +abstract class Endomorphism[A](iniValues: Map[A, Set[A]])(using n:Name) extends Relation[A, A](iniValues: Map[A, Set[A]]) { + + /** + * Remove an element from both the input and output set + * + * @param a the element to remove + */ + override def remove(a: A): Unit = { + apply(a).foreach(remove(a, _)) + inverse(a).foreach(remove(_, a)) + } + + /** + * Provide the reflexive and transitive closure of a by the endomorphism + * + * @param a the input element + * @return the set of all elements indirectly related to a + */ + def closure(a: A): Set[A] = { + lazy val rec: ((A, Set[A])) => Set[A] = immutableHashMapMemo { + s => + if (s._2.contains(s._1)) + Set(s._1) + else + apply(s._1).flatMap(rec(_, s._2 + s._1)) + s._1 + } + rec(a, Set.empty) + } + + /** + * Provide the reflexive and transitive inverse closure of a by the endomorphism + * + * @param a the input element + * @return the set of all elements that indirectly relate to a + */ + def inverseClosure(a: A): Set[A] = { + lazy val rec: ((A, Set[A])) => Set[A] = immutableHashMapMemo { + s => + if (s._2.contains(s._1)) + Set(s._1) + else + inverse(s._1).flatMap(rec(_, s._2 + s._1)) + s._1 + } + rec(a, Set.empty) + } +} diff --git a/src/main/scala/pml/model/relations/LinkRelation.scala b/src/main/scala/pml/model/relations/LinkRelation.scala new file mode 100644 index 0000000..7162a45 --- /dev/null +++ b/src/main/scala/pml/model/relations/LinkRelation.scala @@ -0,0 +1,35 @@ +package pml.model.relations + +import pml.model.hardware.Hardware +import pml.model.service.Service +import sourcecode.Name + +/** + * The endomorphisms used to encode platform links + * + * @param iniValues initial values of the relation + * @tparam A the elements type + */ +case class LinkRelation[A] private(iniValues: Map[A, Set[A]])(using n:Name) extends Endomorphism[A](iniValues) + +object LinkRelation { + /** + * The instances for the links + */ + trait Instances { + + /** + * [[pml.model.service.Service]] linked to [[pml.model.service.Service]] + * @group link_relation + */ + final implicit val ServiceLinkableToService: LinkRelation[Service] = LinkRelation(Map.empty) + + /** + * [[pml.model.hardware.Hardware]] linked to [[pml.model.hardware.Hardware]] + * @group link_relation + */ + final implicit val PLLinkableToPL: LinkRelation[Hardware] = LinkRelation(Map.empty) + + } +} + diff --git a/src/main/scala/pml/model/relations/ProvideRelation.scala b/src/main/scala/pml/model/relations/ProvideRelation.scala new file mode 100644 index 0000000..44214f7 --- /dev/null +++ b/src/main/scala/pml/model/relations/ProvideRelation.scala @@ -0,0 +1,29 @@ +package pml.model.relations + +import pml.model.hardware.Hardware +import pml.model.service.Service +import sourcecode.Name + +/** + * The relations used to encode service providing + * + * @param iniValues initial values of the relation + * @tparam L the left type + * @tparam R the right type + */ +case class ProvideRelation[L, R] private(iniValues: Map[L, Set[R]])(using n:Name) extends Relation[L, R](iniValues) + +object ProvideRelation { + /** + * The instances for the provide relations + */ + trait Instances { + + /** + * [[pml.model.service.Service]] provided by [[pml.model.hardware.Hardware]] + * @group provide_relation + */ + final implicit val PLProvideService: ProvideRelation[Hardware, Service] = ProvideRelation(Map.empty) + + } +} diff --git a/src/main/scala/pml/model/relations/ReflexiveSymmetricEndomorphism.scala b/src/main/scala/pml/model/relations/ReflexiveSymmetricEndomorphism.scala new file mode 100644 index 0000000..63ce663 --- /dev/null +++ b/src/main/scala/pml/model/relations/ReflexiveSymmetricEndomorphism.scala @@ -0,0 +1,18 @@ +package pml.model.relations + +import pml.model.utils.Message +import sourcecode.Name + +abstract class ReflexiveSymmetricEndomorphism[A](iniValues: Map[A, Set[A]])(using n:Name) extends Endomorphism[A](iniValues) { + override def add(a: A, b: A): Unit ={ + super.add(a, b) + super.add(b, a) + super.add(a, a) + super.add(b, b) + } + + override def remove(a: A, b: A): Unit = if(a != b){ + super.remove(a, b) + super.remove(b, a) + } else println(Message.errorReflexivityViolation(a,name)) +} diff --git a/src/main/scala/pml/model/relations/Relation.scala b/src/main/scala/pml/model/relations/Relation.scala new file mode 100644 index 0000000..7783df3 --- /dev/null +++ b/src/main/scala/pml/model/relations/Relation.scala @@ -0,0 +1,136 @@ +package pml.model.relations + +import sourcecode.Name + +import scala.collection.mutable.{HashMap as MHashMap, Set as MSet} + +/** + * Relation between two finite sets + * Warning each one of the set contains the empty set value thus + * R(a) = \emptyset not imply that a \notin R + * + * @param iniValues initial values of the relation + * @tparam L type of the left set + * @tparam R type of the right set + */ +abstract class Relation[L, R](iniValues: Map[L, Set[R]])(using n:Name) { + + val name: String = n.value + + val _values: MHashMap[L, MSet[R]] = MHashMap(iniValues.map(p => p._1 -> MSet(p._2.toSeq: _*)).toSeq: _*) + val _inverse: MHashMap[R, MSet[L]] = _values.flatMap(p => p._2.map(b => b -> MSet(p._1))) + + /** + * Provide the relation a map of edges + * + * @return the map of edges + */ + def edges: Map[L, Set[R]] = (_values.view mapValues { + _.toSet + }).toMap + + /** + * Add the element b to a + * + * @param a the input element + * @param b the new element + */ + def add(a: L, b: R): Unit = { + _values.getOrElseUpdate(a, MSet.empty[R]) += b + _inverse.getOrElseUpdate(b, MSet.empty[L]) += a + } + + /** + * Add a collection of b elements to a + * Warning if the b is empty then all the element linked to a are removed (STRANGE) + * + * @param a the input element + * @param b the collection of new elements + */ + def add(a: L, b: Iterable[R]): Unit = + if (b.nonEmpty) + b.foreach(add(a, _)) + else + _values.getOrElseUpdate(a, MSet.empty[R]).clear() + + /** + * Remove the element b from the relation with a + * + * @param a the input element + * @param b the removed element + */ + def remove(a: L, b: R): Unit = + for (sb <- _values.get(a); sa <- _inverse.get(b)) yield { + sb -= b + sa -= a + } + + /** + * Remove the elements of the collection b from the relation with a + * + * @param a the input element + * @param b the removed elements + */ + def remove(a: L, b: Iterable[R]): Unit = b.foreach(remove(a, _)) + + /** + * Remove a from the relation + * WARNING: this is different from removing all elements in relation with a + * + * @param a the input element + */ + def remove(a: L): Unit = apply(a).foreach(remove(a, _)) + + /** + * Provide the elements in relation with a + * WARNING the function returns an empty set either + * if a is not in the relation + * or if no elements are associated to a + * + * @param a the input element + * @return the set of related elements + */ + def apply(a: L): Set[R] = _values.getOrElse(a, Set.empty).toSet + + /** + * Provide the elements in relation with a id a is in the relation + * + * @param a the input element + * @return the optional set of related elements + */ + def get(a: L): Option[Set[R]] = for {b <- _values.get(a)} yield b.toSet + + /** + * Provide the elements a in relation with b + * + * @param b the output element + * @return the set of related inputs + */ + def inverse(b: R): Set[L] = _inverse.getOrElse(b, Set.empty).toSet + + /** + * Provide the set of all output elements + * + * @return the set of output elements + */ + def targetSet: Set[R] = _values.values.flatten.toSet + + /** + * Provide the set of all input elements + * + * @return the set of input elements + */ + def domain: Set[L] = _values.keys.toSet + +} + +object Relation { + /** + * Trait gathering all relation instances + */ + trait Instances extends LinkRelation.Instances + with UseRelation.Instances + with ProvideRelation.Instances + with AuthorizeRelation.Instances + with RoutingRelation.Instances +} diff --git a/src/main/scala/pml/model/relations/RoutingRelation.scala b/src/main/scala/pml/model/relations/RoutingRelation.scala new file mode 100644 index 0000000..a2d9804 --- /dev/null +++ b/src/main/scala/pml/model/relations/RoutingRelation.scala @@ -0,0 +1,28 @@ +package pml.model.relations + +import pml.model.hardware.Initiator +import pml.model.service.Service +import sourcecode.Name + +/** + * relation used to encode the routing constraints + * + * @param iniValues initial values of the relation + * @tparam L the left type + * @tparam R the right type + */ +case class RoutingRelation[L, R] private(iniValues: Map[L, Set[R]])(using n:Name) extends Relation[L, R](iniValues) + +object RoutingRelation { + /** + * Instances of routing relations + */ + trait Instances { + + /** + * Relation gathering routing constraints + * @group route_relation + */ + final implicit val InitiatorRouting: RoutingRelation[(Initiator, Service, Service), Service] = RoutingRelation(Map.empty) + } +} diff --git a/src/main/scala/pml/model/relations/UseRelation.scala b/src/main/scala/pml/model/relations/UseRelation.scala new file mode 100644 index 0000000..07b9f92 --- /dev/null +++ b/src/main/scala/pml/model/relations/UseRelation.scala @@ -0,0 +1,48 @@ +package pml.model.relations + +import pml.model.hardware.{Initiator, Target} +import pml.model.service.Service +import pml.model.software.{Application, Data} +import sourcecode.Name + +/** + * The relations used to encode the service use + * + * @param iniValues initial values of the relation + * @tparam L the left type + * @tparam R the right type + */ +case class UseRelation[L, R] private(iniValues: Map[L, Set[R]])(using n:Name) extends Relation[L, R](iniValues) + +object UseRelation { + /** + * The instances for the use relations + */ + trait Instances { + /** + * [[pml.model.service.Service]] directly used by [[pml.model.hardware.Initiator]] + * + * @group use_relation + */ + final implicit val InitiatorUseService: UseRelation[Initiator, Service] = UseRelation(Map.empty) + + /** + * [[pml.model.software.Application]] hosted on [[pml.model.hardware.Initiator]] + * @group use_relation + */ + final implicit val SWUseInitiator: UseRelation[Application, Initiator] = UseRelation(Map.empty) + + /** + * [[pml.model.service.Service]] used by [[pml.model.software.Application]] + * @group use_relation + */ + final implicit val SWUseService: UseRelation[Application, Service] = UseRelation(Map.empty) + + /** + * [[pml.model.software.Data]] hosted on [[pml.model.hardware.Target]] + * @group use_relation + */ + final implicit val DataUseTarget: UseRelation[Data, Target] = UseRelation(Map.empty) + + } +} diff --git a/src/main/scala/pml/model/relations/package.scala b/src/main/scala/pml/model/relations/package.scala new file mode 100644 index 0000000..88c415f --- /dev/null +++ b/src/main/scala/pml/model/relations/package.scala @@ -0,0 +1,8 @@ +package pml.model + +/** + * Package containing the implicit relations used to connect PML nodes + * @note these relations should not be used directly. + * To manipulate the relations use the operators detailed in [[pml.operators]] + */ +package object relations diff --git a/src/main/scala/pml/model/service/ArtificialService.scala b/src/main/scala/pml/model/service/ArtificialService.scala new file mode 100644 index 0000000..69f77d7 --- /dev/null +++ b/src/main/scala/pml/model/service/ArtificialService.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.service + +/** + * Base class for artificial services added to encode non topological affects + * @see the possible constructors are provided by [[BaseServiceBuilder]] + * @param name the name of the node + * @group service_class + */ +final class ArtificialService private(val name:Symbol) extends Service + +/** + * Builder of artificial services + * @group builder + */ +object ArtificialService extends BaseServiceBuilder[ArtificialService] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): ArtificialService = new ArtificialService(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/service/BaseServiceBuilder.scala b/src/main/scala/pml/model/service/BaseServiceBuilder.scala new file mode 100644 index 0000000..467c2d0 --- /dev/null +++ b/src/main/scala/pml/model/service/BaseServiceBuilder.scala @@ -0,0 +1,55 @@ +package pml.model.service + +import pml.model.PMLNodeBuilder +import pml.model.utils.Owner +import sourcecode.Name + + +/** + * Base trait for all hardware node builder + * the name of the transporter is implicitly derived from the name of the variable used during instantiation. + * Usually an hardware can be constructed without arguments, where T + * can be [[Load]], [[Store]], [[ArtificialService]] + * {{{ + * val myService = T() + * }}} + * + * It is also possible to give a specific name, for instance when creating the component in a loop then the following + * constructor can bee used + * {{{ + * val serviceSeq = for { i <- O to N } yield T(s"myService\$i") + * }}} + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + * @tparam T the concrete type of built object + * @group builder + **/ +trait BaseServiceBuilder[T <: Service] extends PMLNodeBuilder[T] { + + /** + * The builder that must be implemented by specific builder + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): T + + /** + * A service can be defined by the name provided by the implicit declaration context + * (the name of the value enclosing the object) + * + * @param name the implicit service name + * @param owner implicitly retrieved name of the platform + * @return the service + */ + def apply()(implicit name: Name, owner: Owner): T = apply(Symbol(name.value)) + + /** + * A service can be defined by its name + * + * @param name the service name + * @param owner implicitly retrieved name of the platform + * @return the service + */ + def apply(name: Symbol)(implicit owner: Owner): T = { + _memo.getOrElseUpdate((name, owner.s), builder(name)) + } +} diff --git a/src/main/scala/pml/model/service/Load.scala b/src/main/scala/pml/model/service/Load.scala new file mode 100644 index 0000000..e4eae94 --- /dev/null +++ b/src/main/scala/pml/model/service/Load.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.service + +/** + * Base class for load services + * @see the possible constructors are provided by [[BaseServiceBuilder]] + * @param name the name of the node + * @group service_class + */ +final class Load private(val name: Symbol) extends Service + +/** + * Builder of loads + * @group builder + */ +object Load extends BaseServiceBuilder[Load] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): Load = new Load(name) + +} diff --git a/src/main/scala/pml/model/service/Service.scala b/src/main/scala/pml/model/service/Service.scala new file mode 100644 index 0000000..2005509 --- /dev/null +++ b/src/main/scala/pml/model/service/Service.scala @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.service + +import pml.model.PMLNode +import sourcecode.Enclosing + +/** + * Base class for the load and store services provided by all physical components + * + * @param enclosing the implicit context that can be + * used to find the source code location of the node definition + * @group service_class + */ +abstract class Service private[service](implicit enclosing: Enclosing) extends PMLNode diff --git a/src/main/scala/pml/model/service/Store.scala b/src/main/scala/pml/model/service/Store.scala new file mode 100644 index 0000000..491fa59 --- /dev/null +++ b/src/main/scala/pml/model/service/Store.scala @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.service + +/** + * Base class for store services + * @see the possible constructors are provided by [[BaseServiceBuilder]] + * @param name the name of the node + * @group service_class + */ +final class Store private(val name: Symbol) extends Service + +/** + * Builder of stores + * @group builder + */ +object Store extends BaseServiceBuilder[Store] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): Store = new Store(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/service/package.scala b/src/main/scala/pml/model/service/package.scala new file mode 100644 index 0000000..03f6ff3 --- /dev/null +++ b/src/main/scala/pml/model/service/package.scala @@ -0,0 +1,9 @@ +package pml.model + +/** + * Package containing all service PML nodes + * [[Load]], [[Store]] and [[ArtificialService]] can be instantiated when extra services are needed. + * @note hardware component provides by default a load and store service. + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + */ +package object service diff --git a/src/main/scala/pml/model/software/Application.scala b/src/main/scala/pml/model/software/Application.scala new file mode 100644 index 0000000..a48a9c3 --- /dev/null +++ b/src/main/scala/pml/model/software/Application.scala @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.software + +import pml.model.PMLNode + +/** + * Class for the application executed on initiators + * @see the possible constructors are provided by [[BaseSoftwareNodeBuilder]] + * @param name the name of the node + * @group software_class + */ +final class Application(val name:Symbol) extends PMLNode + +/** + * Builder of [[Application]] + * @group builder + */ +object Application extends BaseSoftwareNodeBuilder[Application] { + + /** + * Direct builder from name + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): Application = new Application(name) + +} \ No newline at end of file diff --git a/src/main/scala/pml/model/software/BaseSoftwareNodeBuilder.scala b/src/main/scala/pml/model/software/BaseSoftwareNodeBuilder.scala new file mode 100644 index 0000000..3c6d32c --- /dev/null +++ b/src/main/scala/pml/model/software/BaseSoftwareNodeBuilder.scala @@ -0,0 +1,54 @@ +package pml.model.software + +import pml.model.PMLNodeBuilder +import pml.model.utils.Owner +import sourcecode.Name + +/** + * Base trait for all hardware node builder + * the name of the transporter is implicitly derived from the name of the variable used during instantiation. + * Usually an hardware can be constructed without arguments, where T + * can be [[Application]], [[Data]] + * {{{ + * val mySoftware = T() + * }}} + * + * It is also possible to give a specific name, for instance when creating the component in a loop then the following + * constructor can bee used + * {{{ + * val softwareSeq = for { i <- O to N } yield T(s"mySoftware\$i") + * }}} + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + * @tparam T the concrete type of built object + * @group builder + **/ +trait BaseSoftwareNodeBuilder[T <: Application] extends PMLNodeBuilder[T] { + + /** + * The builder that must be implemented by specific builder + * @param name the name of the object + * @return the object + */ + protected def builder(name: Symbol): T + + /** + * A software component can be defined only its name + * + * @param name the software name + * @param owner implicitly retrieved name of the platform + * @return the software + */ + def apply(name: Symbol)(implicit owner: Owner): T = + _memo.getOrElseUpdate((owner.s, name), builder(name)) + + /** + * A software component can be defined by the name provided by the implicit declaration context + * (the name of the value enclosing the object) + * + * @param name the implicit software name + * @param owner implicitly retrieved name of the platform + * @return the software + */ + def apply()(implicit name: Name, owner: Owner): T = + apply(Symbol(name.value)) +} diff --git a/src/main/scala/pml/model/software/Data.scala b/src/main/scala/pml/model/software/Data.scala new file mode 100644 index 0000000..dceff61 --- /dev/null +++ b/src/main/scala/pml/model/software/Data.scala @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.model.software + +import pml.model.utils.Owner +import pml.model.{PMLNode, PMLNodeBuilder} +import sourcecode.Name + +/** + * Util class to represent a data owned by a target + * @see the possible constructors are provided by [[BaseSoftwareNodeBuilder]] + * @param name the name of the data + * @group software_class + */ +final class Data private(val name: Symbol) extends PMLNode { + + override def toString: String = name.name +} + +/** + * Builder of [[Data]] + * @group builder + */ +object Data extends PMLNodeBuilder[Data] { + + /** + * A data is defined by its name + * @param name the name of the data + * @param owner the implicit name of the platform + * @return the data + */ + def apply(name: Symbol)(implicit owner: Owner): Data = { + _memo.getOrElseUpdate((owner.s, name), new Data(name)) + } + + /** + * A data can be defined by the implicit name used during the definition + * (name of the variable enclosing the object) + * @param name the implicit name of the data + * @param owner the implicit name of the platform + * @return the data + */ + def apply()(implicit name: Name, owner: Owner): Data = + apply(Symbol(name.value)) +} diff --git a/src/main/scala/pml/model/software/package.scala b/src/main/scala/pml/model/software/package.scala new file mode 100644 index 0000000..5798a69 --- /dev/null +++ b/src/main/scala/pml/model/software/package.scala @@ -0,0 +1,9 @@ +package pml.model + +/** + * Package containing software PML nodes. + * @note The [[Application]] and [[Data]] must be instantiated in a [[pml.model.hardware.Platform]] + * and cannot specialised in PML models. + * @see example of usage are available in [[pml.examples.simpleKeystone.SimpleSoftwareAllocation]] + */ +package object software diff --git a/src/main/scala/pml/model/utils/Message.scala b/src/main/scala/pml/model/utils/Message.scala new file mode 100644 index 0000000..02f74ae --- /dev/null +++ b/src/main/scala/pml/model/utils/Message.scala @@ -0,0 +1,112 @@ +package pml.model.utils + +import pml.model.configuration.TransactionLibrary.{UserScenarioId, UserTransactionId} +import pml.model.hardware.{Hardware, Initiator, Target} +import pml.model.service.Service +import pml.model.software.Application +import views.interference.model.specification.InterferenceSpecification.{PhysicalScenarioId, PhysicalTransaction, PhysicalTransactionId} + +import scala.collection.mutable + +/** + * Listing all information, warning or error messages displayed to the user + */ +object Message { + inline def impossibleTransactionWarning(userName: UserTransactionId): String = + s"[WARNING] The physical transaction $userName is not possible, check your link and route constraints" + + inline def impossibleRouteWarning(t:Service, from:Option[Application]): String = + s"""[WARNING] The target service $t cannot be reached ${if (from.isDefined) s"from ${from.get}" else ""}""" + + inline def multiPathTransactionWarning(userName: UserTransactionId, list: Iterable[(PhysicalTransactionId, PhysicalTransaction)]): String = + s"""[WARNING] The transaction $userName addresses multiple physical transactions: + |${list.map(_._1).mkString("\n")} + |so $userName will be considered as a scenario""".stripMargin + + inline def multiPathRouteWarning(from:Service, to:Service, transactions:Set[PhysicalTransaction]):String = { + s"""[WARNING] Multiple paths have been detected from $from to $to + |${transactions.map(_.mkString("<->")).mkString("\n")}""".stripMargin + } + + inline def transactionNoInLibraryWarning(name: PhysicalTransactionId): String = + s"[WARNING] The physical transaction $name is not in the library" + + inline def transactionHasSeveralNameWarning(name: PhysicalTransactionId, names: Iterable[UserTransactionId]): String = + s"[WARNING] The physical transaction $name has ${names.size} distinct names ${names.map(_.id.name).mkString(", ")}" + + inline def impossibleScenarioWarning(userName: UserScenarioId): String = + s"[WARNING] The physical scenario $userName is not possible, check your link and route constraints" + + inline def multiPathScenarioWarning(userName: UserScenarioId, list: Iterable[(PhysicalTransactionId, PhysicalTransaction)]): String = + s"""[WARNING] Some transactions in scenario $userName addresses multiple physical transactions: + |${list.map(_._1).mkString("\n")} + |all paths will be considered in the scenario, consider routing constraints to avoid multi-path""".stripMargin + + inline def scenarioNotInLibraryWarning(name: PhysicalScenarioId): String = + s"[WARNING] The physical scenario $name is considered but not defined in the library" + + inline def applicationNotUsingServicesWarning(a:Application): String = + s"[WARNING] $a is not using any service" + + inline def applicationNotAllocatedWarning(a:Application): String = + s"[WARNING] $a is not allocated on any initiator" + + inline def noServiceInitiatorWarning(a:Application, s:Initiator): String = + s"[WARNING] $a is allocated on $s that does not provide any basic service" + + inline def uselessRoutingConstraintWarning(from:Hardware, to:Hardware): String = + s"[WARNING] Useless routing constraints: $from services are not linked to the ones of $to" + + val cyclicGraphWarning: String = "[WARNING] The paths computed on the graph my be incorrect since the graph is cyclic" + + inline def cycleWarning(visited:Seq[(Any,Any)], ini:Any, tgt:Any) : String = + s"[WARNING] cycle found on edge ${visited.map(p => s"${p._1} -> ${p._2}").mkString(" , ")} from initiator $ini to reach $tgt" + + inline def successfulExportInfo(name:Any, time:Any): String = + s"[INFO] $name exported successfully in $time s" + + inline def analysisResultFoundInfo(folder:Any, platform:Any) : String = + s"[INFO] $folder already contains result files for $platform, computation discarded" + + inline def successfulModelBuildInfo(platform:Any, time:Any) : String = + s"[INFO] $platform MonoSat model successfully built in $time s" + + inline def startingNonExclusiveScenarioEstimationInfo(platform:Any) : String = + s"[INFO] Starting $platform estimation of number of non exclusive scenarios" + + inline def successfulNonExclusiveScenarioEstimationInfo(platform:Any, time:Any) : String = + s"[INFO] $platform estimation of number of non exclusive scenarios completed in $time s" + + inline def iterationCompletedInfo(i:Any, n:Any, time:Any): String = + s"[INFO] Iteration $i / $n completed in $time s" + + inline def analysisCompletedInfo(analysis:Any, time:Any): String = + s"[INFO] $analysis completed in $time s" + + inline def iterationResultsInfo(isFree: Boolean, computed: mutable.Map[Int, Int], over: Map[Int, BigInt]): String = + s"""[INFO] Interference ${if (isFree) "free " else ""}computed so far + |${printScenarioNumber(computed, over)}""".stripMargin + + inline def printScenarioNumber(computed: mutable.Map[Int, Int], over: Map[Int, BigInt]): String = { + s"""${ + computed + .toSeq + .sortBy(_._1) + .map(p => s"[INFO] size ${p._1}: ${p._2} over ${over(p._1)} (${if(over(p._1) == 0) "0" else if (p._2 * 100 / over(p._1) == 0) "< 1" else math.round(p._2 * 100 / over(p._1).toDouble).toInt}%)") + .mkString("\n") + } + |""".stripMargin + } + + inline def errorReflexivityViolation(x:Any, in:Any): String = + s"[WARNING] cannot remove edge $x -> $x in relation $in since it should be reflexive" + + inline def errorAntiReflexivityViolation(x: Any, in: Any): String = + s"[WARNING] cannot add edge $x -> $x in relation $in since it should be anti-reflexive" + + inline def errorAntiSymmetryViolation(l:Any, r:Any, in:Any): String = + s"[ERROR] cannot add edge $l -> $r in relation $in since edge $r -> $l already exists and $in should be antisymmetric" + + inline def successfulITFDifferenceExportInfo(size:Any, x:Any, y:Any, file:Any): String = + s"[INFO] The $size-itf differences between $x and $y have been exported to $file" +} diff --git a/src/main/scala/pml/model/utils/Owner.scala b/src/main/scala/pml/model/utils/Owner.scala new file mode 100644 index 0000000..479d783 --- /dev/null +++ b/src/main/scala/pml/model/utils/Owner.scala @@ -0,0 +1,8 @@ +package pml.model.utils + +/** + * Utility class to track hierarchy in the model + * + * @param s the owner's name + */ +case class Owner(s: Symbol) diff --git a/src/main/scala/pml/model/utils/package.scala b/src/main/scala/pml/model/utils/package.scala new file mode 100644 index 0000000..e918526 --- /dev/null +++ b/src/main/scala/pml/model/utils/package.scala @@ -0,0 +1,6 @@ +package pml.model + +/** + * Package containing classes and methods + */ +package object utils diff --git a/src/main/scala/pml/operators/AsTransaction.scala b/src/main/scala/pml/operators/AsTransaction.scala new file mode 100644 index 0000000..f5ea11b --- /dev/null +++ b/src/main/scala/pml/operators/AsTransaction.scala @@ -0,0 +1,41 @@ +package pml.operators + +import pml.model.service.* +import pml.operators.* +import pml.model.software.Application +import AsTransaction.TransactionParam +import pml.model.hardware.Initiator + +trait AsTransaction[A] { + def apply(a: => A): TransactionParam +} + +object AsTransaction{ + type TransactionParam = (() => Set[(Service, Service)], () => Set[Application]) + + trait Ops { + def TransactionParam[A](a: => A)(using ev:AsTransaction[A]): TransactionParam = ev(a) + } + + /** + * Utility function to convert an a set of application/target service to the set of initial/target services + * @return the set of initial/target services and of applications invoking them + */ + + given applicationUsed[T<: Load | Store] (using u:Used[Application,Initiator], p:Provided[Initiator,T]) : AsTransaction[Set[(Application,T)]] with { + def apply(a: => Set[(Application, T)]): (() => Set[(Service, Service)], () => Set[Application]) = ( + () => { + a.flatMap(as => as._1.hostingInitiators.flatMap(_.provided[T]).map(_ -> as._2)) + }, + () => a.map(_._1)) + } + + given initiatorUsed[T<: Load | Store] (using p: Provided[Initiator, T]): AsTransaction[Set[(Initiator, T)]] with { + def apply(a: => Set[(Initiator, T)]): (() => Set[(Service, Service)], () => Set[Application]) = + (() => { + a.flatMap(as => as._1.provided[T].map(_ -> as._2)) + }, + () => Set.empty) + } + +} diff --git a/src/main/scala/pml/operators/Deactivate.scala b/src/main/scala/pml/operators/Deactivate.scala new file mode 100644 index 0000000..7c26efc --- /dev/null +++ b/src/main/scala/pml/operators/Deactivate.scala @@ -0,0 +1,95 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.operators + +import pml.model.hardware.{Hardware, Initiator} +import pml.model.relations.{LinkRelation, ProvideRelation, UseRelation} +import pml.model.service.Service +import pml.model.software.Application + +/** + * Base trait for deactivation operations + * + * @tparam T the type of the deactivatable component + */ +trait Deactivate[-T] { + def apply(a: T): Unit +} + +/** + * Extension methods and inferences rules + */ +object Deactivate { + + def apply[T](using m: Deactivate[T]): Deactivate[T] = m + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element x of type T is deactavitable then the operator can be used as follows + * {{{ + * x.deactivate + * }}} + * + * @note currently any [[pml.model.hardware.Hardware]] or [[pml.model.software.Application]] is deactivatable + * and the deactivation can be made only within a platform container + */ + + trait Ops { + extension[T: Deactivate] (a: T) { + def deactivated: Unit = Deactivate[T](a) + } + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + + /** + * An application can be deactivated by removing all the services its uses + * @return the implementation of application deactivation + */ + given (using uSL: UseRelation[Application, Service], uAS: UseRelation[Application, Initiator]): Deactivate[Application] with { + def apply(a: Application): Unit = { + uSL.remove(a) + uAS.remove(a) + } + } + + /** + * A physical component can be deactivated by removing all the services its provides, the software using it and + * the services connected to his + * @return the implementation of physical component deactivation + */ + given (using l: LinkRelation[Service], p: ProvideRelation[Hardware, Service], u: UseRelation[Application, Service]): Deactivate[Hardware] with { + def apply(a: Hardware): Unit = { + p(a) foreach { + s => { + l.remove(s) + u.inverse(s) foreach { + u.remove(_, s) + } + } + } + p.remove(a) + } + } +} \ No newline at end of file diff --git a/src/main/scala/pml/operators/Link.scala b/src/main/scala/pml/operators/Link.scala new file mode 100644 index 0000000..1a674dc --- /dev/null +++ b/src/main/scala/pml/operators/Link.scala @@ -0,0 +1,163 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.operators + +import pml.model.hardware.{Hardware, Initiator, Target, Transporter} +import pml.model.relations.LinkRelation +import pml.model.service.{Load, Service, Store} + +import scala.reflect.{ClassTag, classTag} + +/** + * Base trait for link operation + * + * @tparam L the left type + * @tparam R the right type + */ +trait Link[L, R] { + def link(a: L, b: R): Unit + def unlink(a: L, b: R): Unit +} + +/** + * Extension methods and inferences rules of high priority + */ +object Link { + + protected sealed trait HardwareLink[-L,-R]{ + def apply(x:L|R):Hardware + } + given HardwareLink[Initiator, Target] with { + def apply(x: Initiator|Target): Hardware = x + } + given HardwareLink[Initiator, Transporter] with { + def apply(x: Initiator|Transporter): Hardware = x + } + given HardwareLink[Transporter, Transporter] with { + def apply(x: Transporter): Hardware = x + } + given HardwareLink[Transporter, Target] with { + def apply(x: Transporter | Target): Hardware = x + } + + protected sealed trait ServiceLink[L,R]{ + def apply(x: L|R): Service + } + given ServiceLink[Load,Load] with { + def apply(x: Load): Service = x + } + given ServiceLink[Store,Store] with { + def apply(x: Store): Service = x + } + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element x of type T is linkable then the operator can be used as follows + * + * to link an element l to an element r + * {{{ l link r }}} + * to unlink an element l with an element r + * {{{ l unlink r }}} + * + * @note currently any [[pml.model.hardware.Hardware]] or [[pml.model.service.Service]] are linkable + * with soundness restriction. Additionally, the link can be made only within a platform container + * @see usage are available in [[pml.examples.simpleKeystone.SimpleKeystonePlatform]] + */ + trait Ops { + + extension[L] (self: L) { + /** + * PML keyword to link two objects + * + * @param b the other object + * @param linkable the proof that self and b can be linked + * @tparam R the type of the other object + */ + def link[R](b: R)(using linkable: Link[L, R]): Unit = linkable.link(self, b) + + /** + * PML keyword to unlink two objects + * + * @param b the other object + * @param linkable the proof that self and b can be linked + * @tparam R the type of the other object + */ + def unlink[R](b: R)(using linkable: Link[L, R]): Unit = linkable.unlink(self, b) + + } + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * A linking implementation between two types can be derived from an endomorphism over a supertype + * + * @return the implementation of the link + */ + given [LS,RS] (using canLink:ServiceLink[LS,RS], l: LinkRelation[Service]): Link[LS,RS] with { + def link(a: LS, b: RS): Unit = l.add(canLink(a), canLink(b)) + def unlink(a: LS, b: RS): Unit = l.remove(canLink(a), canLink(b)) + } + + /** + * A linking implementation can be provided for all physical component + * + * @return the implementation of the link for two physical components + */ + given [LH,RH] (using canLink:HardwareLink[LH,RH], + mAB: LinkRelation[Hardware], + mLL: Link[Load, Load], + mSS: Link[Store, Store], + pAS: Provided[LH, Store], + pAL: Provided[LH, Load], + pBS: Provided[RH, Store], + pBL: Provided[RH, Load]): Link[LH, RH] with { + def link(a: LH, b: RH): Unit = { + mAB.add(canLink(a), canLink(b)) + a.loads foreach { l => + b.loads.foreach { + l link _ + } + } + a.stores foreach { s => + b.stores.foreach { + s link _ + } + } + } + + def unlink(a: LH, b: RH): Unit = { + mAB.remove(canLink(a), canLink(b)) + a.loads foreach { l => + b.loads.foreach { + l unlink _ + } + } + a.stores foreach { s => + b.stores.foreach { + s unlink _ + } + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/pml/operators/Linked.scala b/src/main/scala/pml/operators/Linked.scala new file mode 100644 index 0000000..b634213 --- /dev/null +++ b/src/main/scala/pml/operators/Linked.scala @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.operators + +import pml.model.relations.LinkRelation +import pml.model.service.{Load, Service, Store} +import scala.reflect._ +/** + * Base trait for linked operation + * @tparam L the left type + * @tparam R the right type + */ +trait Linked[L, R] { + def apply(a: L): Set[R] + def inverse(b: R): Set[L] +} + +/** + * Extension methods and inferences rules + */ +object Linked { + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element l of type L is linked to other elements of type R then the operator can be used + * + * To access to the element of type R linked to l + * {{{l.linked[R]}}} + * To access to the element of type R pointing to l + * {{{ l.inverse[R] }}} + * @note Linked operators is an advanced feature and should not be necessary for basic models + */ + trait Ops { + + /** + * Extension method + */ + extension [L](self: L) { + + /** + * PML keyword to retrieve elements linked to self + * @param linked the proof that elements of type R can be linked to self + * @tparam R the type of linked elements + * @return the set of linked elements + */ + def linked[R](using linked: Linked[L, R]) : Set[R] = linked(self) + + /** + * PML keyword to retrieve elements pointing to self + * @param linked the proof that elements of type L can point to self + * @tparam R the type of pointing elements + * @return the set of pointing elements + */ + def inverse[R](using linked: Linked[R,L]) : Set[R] = linked.inverse(self) + } + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * A linked implementation can be derived from an endomorphism over a type + * + * @return the implementation of the linked + */ + given[L](using l: LinkRelation[L]): Linked[L, L] with { + def apply(a: L): Set[L] = l(a) + def inverse(b: L): Set[L] = l.inverse(b) + } + + /** + * A linked implementation over loads can be derived from an endomorphism over a services + * @return the implementation of the linked + */ + given [T <: Load | Store : Typeable](using l: Linked[Service, Service]): Linked[T, T] with { + def apply(a: T): Set[T] = l(a) collect { case l: T => l } + def inverse(b: T): Set[T] = l.inverse(b) collect { case l: T => l } + } +} \ No newline at end of file diff --git a/src/main/scala/pml/operators/Merge.scala b/src/main/scala/pml/operators/Merge.scala new file mode 100644 index 0000000..3891c21 --- /dev/null +++ b/src/main/scala/pml/operators/Merge.scala @@ -0,0 +1,167 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.operators + +import pml.model.service.Service + +/** + * Concatenation operation over two types + * FIXME IS THIS REALLY HELPFUL + * @tparam L left type + * @tparam R right type + */ +trait Merge[-L, -R] { + type Result + def apply(l:L, r:R) : Result +} + +/** + * Extension methods and inferences rules + */ +object Merge { + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element l is mergeable with an element r then the following operator can be used + * {{{l and r}}} + */ + trait Ops { + + /** + * Extension method class + * + * @param x the element on which keyword can be used + * @tparam L the concrete type of the element + */ + extension[L](x:L) { + + /** + * PML keyword to merge an element with x + * @param y the other element + * @param ev the proof that x can be merged with y + * @tparam R the type of y + * @tparam O the resulting type of the merge + * @return the merge of x and y + */ + def and[R,O](y:R)(implicit ev:Merge.Aux[L,R,O]) : O = ev(x,y) + } + } + /** + * Util type to alleviate type unification problem + * @tparam L left type + * @tparam R right type + * @tparam O result type for the concatenation, functionally defined by inner type Result + */ + type Aux[L,R,O] = Merge[L,R] { + type Result = O + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * Two sets of objects providing basic services can be concatenated as a set of basic services + * @param pT the relation capturing the basic services provided by L + * @param pU the relation capturing the basic services provided by R + * @tparam L the left type + * @tparam R the right type + * @return the set of basic services provided by collections of L and R + */ + implicit def setsTAreMergeable[L,R](implicit pT:Provided[L,Service], pU:Provided[R,Service]): Aux[Set[L],Iterable[R],Set[Service]] = new Merge[Set[L],Iterable[R]]{ + type Result = Set[Service] + def apply(l: Set[L], r: Iterable[R]): Set[Service] = l.services ++ r.services + } + + /** + * Two sets of basic services can be concatenated as a set of basic services + * @tparam L the left type + * @tparam R the right type + * @return the set of basic services + */ + implicit def setsAreMergeable[L<:Service,R<:Service]: Aux[Set[L],IterableOnce[R],Set[Service]] = new Merge[Set[L],IterableOnce[R]]{ + type Result = Set[Service] + def apply(l: Set[L], r: IterableOnce[R]): Set[Service] = l ++ r + } + + /** + * A of basic services and an object providing services can be concatenated as a set of basic services + * @param p the relation capturing the basic services provided by R + * @tparam L the left type + * @tparam R the right type + * @return the set of basic services + */ + implicit def setAndValAreMergeable[L<:Service,R](implicit p:Provided[R,Service]): Aux[Set[L],R,Set[Service]] = new Merge[Set[L],R]{ + type Result = Set[Service] + def apply(l: Set[L], r: R): Set[Service] = l ++ r.services + } + + implicit def valAndSetAreMergeable[L,R<:Service](implicit provided: Provided[L,Service]): Aux[L,Set[R],Set[Service]] = new Merge[L,Set[R]]{ + type Result = Set[Service] + def apply(l: L, r: Set[R]): Set[Service] = r ++ l.services + } + + /** + * Two objects providing services can be concatenated as a set of basic services + * @param pT the relation capturing the basic services provided by L + * @param pU the relation capturing the basic services provided by R + * @tparam L the left type + * @tparam R the right type + * @return the set of basic services + */ + implicit def valAndValAreMergeable[L,R](implicit pT: Provided[L,Service], pU: Provided[R,Service]): Aux[L,R,Set[Service]] = new Merge[L,R]{ + type Result = Set[Service] + def apply(l: L, r: R): Set[Service] = r.services ++ l.services + } + + /** + * A basic and an object providing services can be concatenated as a set of basic services + * @param pU the relation capturing the basic services provided by R + * @tparam L the left type + * @tparam R the right type + * @return the set of basic services + */ + implicit def basicAndValAreMergeable[L<:Service,R](implicit pU: Provided[R,Service]): Aux[L,R,Set[Service]] = new Merge[L,R]{ + type Result = Set[Service] + def apply(l: L, r: R): Set[Service] = r.services + l + } + + implicit def valAndBasicAreMergeable[L,R<:Service](implicit pT: Provided[L,Service]): Aux[L,R,Set[Service]] = new Merge[L,R]{ + type Result = Set[Service] + def apply(l: L, r: R): Set[Service] = l.services + r + } + + /** + * A set of basic services and a basic service can be concatenated as a set of basic services + * @tparam L the left type + * @tparam R the right type + * @return the set of basic services + */ + implicit def setsAndBasicAreMergeable[L<:Service,R<:Service]: Aux[Set[L],R,Set[Service]] = new Merge[Set[L],R]{ + type Result = Set[Service] + def apply(l: Set[L], r: R): Set[Service] = l ++ Set(r) + } + + implicit def basicAndSetAreMergeable[L<:Service,R<:Service]: Aux[L,Set[R],Set[Service]] = new Merge[L,Set[R]]{ + type Result = Set[Service] + def apply(l: L, r: Set[R]): Set[Service] = r ++ Set(l) + } +} diff --git a/src/main/scala/pml/operators/Provided.scala b/src/main/scala/pml/operators/Provided.scala new file mode 100644 index 0000000..bf322dc --- /dev/null +++ b/src/main/scala/pml/operators/Provided.scala @@ -0,0 +1,244 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package pml.operators + +import pml.model.hardware._ +import pml.model.relations.ProvideRelation +import pml.model.service.{Load, Service, Store} +import pml.model.software.Data +import pml.model.utils.Owner + +import scala.reflect._ + +/** + * Base trait for provide operator + * + * @tparam L the provider (left) type + * @tparam R the provided (right) type + */ +trait Provided[L, R] { + def apply(a: L): Set[R] + + def owner(b: R): Set[L] +} + +/** + * Extension methods and inferences rules + */ +object Provided { + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element l of type L can provide element of type R then the operator can be used + * + * To access to the load services provided by an element l (e.g. a [[pml.model.hardware.Hardware]]) + * {{{ l.loads }}} + * To access to the store services provided by an element l + * {{{ l.stores }}} + * To access to the services provided by an element l + * {{{ l.services }}} + * To access to the initiators provided by an element l (e.g. a [[pml.model.hardware.Platform]]) + * {{{ l.initiators }}} + * To access to the targets provided by an element l + * {{{ l.targets }}} + * To access to the transporters provided by an element l + * {{{ l.transporters }}} + * To access to the hardware provided by an element l + * {{{ l.hardware }}} + * To access to the initiator providing an element l (e.g. a [[pml.model.service.Service]]) + * {{{ l.initiatorOwner }}} + * To access to the target providing an element l + * {{{ l.targetOwner }}} + * To access to the transporter providing an element l + * {{{ l.transporterOwner }}} + * To access to the hardware providing an element l + * {{{ l.hardwareOwner }}} + * To check if an hardware r is providing an element l + * {{{ l.hardwareOwnerIs(r) }}} + * @note currently provide operators are maily applicable on [[pml.model.hardware.Hardware]] and [[pml.model.software.Data]] + * @see provide usage can be found in [[views.interference.examples.simpleKeystone.SimpleKeystonePhysicalTableBasedInterferenceSpecification]] + */ + trait Ops { + + /** + * Extension methods + */ + extension [L](self: L) { + + def provided[U](using ev: Provided[L, U]): Set[U] = ev(self) + + /** + * PML keyword to get the loads provided by self + * @param ev the proof that self provides loads + * @return the set of provided loads + */ + def loads(using ev: Provided[L, Load]): Set[Load] = ev(self) + + /** + * PML keyword to get the stores provided by self + * @param ev the proof that self provides stores + * @return the set of provided stores + */ + def stores(using ev: Provided[L, Store]): Set[Store] = ev(self) + + /** + * PML keyword to get the services provided by self + * @param ev the proof that self provides services + * @return the set of provided services + */ + def services(using ev: Provided[L, Service]): Set[Service] = ev(self) + + } + + /** + * Extension methods + */ + extension [L <: Platform](self:L) { + + def initiators(using ev: Provided[L, Hardware]): Set[Initiator] = ev(self).collect({case x: Initiator => x}) + + def targets(using ev: Provided[L, Hardware]): Set[Target] = ev(self).collect({case x: Target => x}) + + def hardware(using ev: Provided[L, Hardware]): Set[Hardware] = ev(self) + + def transporters(using ev: Provided[L, Hardware]): Set[Transporter] = ev(self).collect({case x: Transporter => x}) + + /** + * Provide all the physical elements declared inside the composite + * + * @return set of declared component + */ + def directHardware: Set[Hardware] = { + import self._ + Initiator.allDirect ++ Target.allDirect ++ Virtualizer.allDirect ++ SimpleTransporter.allDirect ++ Composite.allDirect + } + } + + /** + * Extension methods for iterable + */ + extension [L](self: Iterable[L]) { + def loads(using ev: Provided[L, Load]): Set[Load] = self.flatMap(ev.apply).toSet + + def stores(using ev: Provided[L, Store]): Set[Store] = self.flatMap(ev.apply).toSet + + def services(using ev: Provided[L, Service]): Set[Service] = self.flatMap(ev.apply).toSet + } + + /** + * Extension methods for owner + */ + extension[R](self: R) { + + def hardwareOwner(using ev: Provided[Hardware, R]): Set[Hardware] = ev.owner(self) + + def hardwareOwnerIs(that: Hardware)(using ev: Provided[Hardware, R]): Boolean = hardwareOwner.contains(that) + + def targetOwner(using ev: Provided[Target, R]): Set[Target] = ev.owner(self) + + def initiatorOwner(using ev: Provided[Initiator, R]): Set[Initiator] = ev.owner(self) + + def transporterOwner(using ev: Provided[Transporter, R]): Set[Transporter] = ev.owner(self) + } + + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + /** + * An implementation of the provide operator between two object is derivable from a relation + * + * @return the implementation of the provided + */ + given[L, R](using p: ProvideRelation[L, R]): Provided[L, R] with { + def apply(a: L): Set[R] = p(a) + + def owner(b: R): Set[L] = p.inverse(b) + } + + /** + * An implementation of the services provided by an hardware component is derivable from the + * services provided by a physical component + * + * @return the implementation of the provided + */ + given[T <: Hardware : Typeable, S <: Service : Typeable](using ev: Provided[Hardware, Service]): Provided[T, S] with { + def apply(a: T): Set[S] = ev(a) collect { + case s : S => s + } + + def owner(b: S): Set[T] = ev.owner(b) collect { + case s : T => s + } + } + + /** + * An implementation of the initiators provided by a platform + * + * @return the implementation of the provided + */ + given[L <: Platform: Typeable]: Provided[L, Hardware] with { + + def apply(a: L): Set[Hardware] = { + import a._ + Initiator.all ++ Target.all ++ Virtualizer.all ++ SimpleTransporter.all + } + + def owner(b: Hardware): Set[L] = Platform.all.collect { + case p : L if (Initiator.all(p.currentOwner) + ++ Target.all(p.currentOwner) + ++ Virtualizer.all(p.currentOwner) + ++ SimpleTransporter.all(p.currentOwner) ).contains(b) => p + } + } + + /** + * An implementation of the services provided by platforms + */ + given[T <: Platform : Typeable] : Provided[T, Service] with { + def apply(a: T): Set[Service] = a.PLProvideService.targetSet + + def owner(b: Service): Set[T] = Platform.all.collect { + case p : T if p.PLProvideService.targetSet.contains(b) => p + } + } + + /** + * An implementation of the services provided by a target on which a data is allocated is derivable from the + * services provided by a target and the name if the platform + * + * @return the implementation of the provided + */ + given[T](using p: Provided[Target, T], u:Used[Data,Target], r:Used[Target, Data], dOwner: Owner): Provided[Data, T] with { + def apply(a: Data): Set[T] = + for { + t <- a.hostingTargets + b <- p(t) + } yield b + + def owner(b: T): Set[Data] = + for { + t <- b.targetOwner + d <- t.hostedData + } yield d + } +} \ No newline at end of file diff --git a/src/main/scala/pml/operators/Restrict.scala b/src/main/scala/pml/operators/Restrict.scala new file mode 100644 index 0000000..3ab729a --- /dev/null +++ b/src/main/scala/pml/operators/Restrict.scala @@ -0,0 +1,362 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.operators + +import pml.model.hardware.{Hardware, Initiator, Platform} +import pml.model.relations.{AuthorizeRelation, LinkRelation, RoutingRelation, UseRelation} +import pml.model.service.{Load, Service, Store} +import pml.model.software.Application +import pml.model.utils.Message + +import scala.collection.mutable.HashMap as MHashMap + +/** + * Base trait for restrict operator used to restrict the connection graph + * of elements L to the elements R that are used + * + * @tparam L the left type + * @tparam R the right type + */ +trait Restrict[L, R] { + + private val _memo = MHashMap.empty[(Service, Initiator, Service, Service), Boolean] + + private val _memoRoute = MHashMap.empty[(Service, Initiator, Service, Service), Boolean] + + def usedForTgt[U <: Service](tgt: U, ini: Initiator, from: U, to: U)(implicit + lU: Linked[U, U], + p: Provided[Initiator, Service], + r: RoutingRelation[(Initiator, Service, Service), Service]): Boolean = + _memo.getOrElseUpdate((tgt, ini, from, to), usedForTgt(tgt, ini, from, to, Seq.empty[(U, U)])) + + /** + * The service x is used to reach target from the initiator (knowing a set of visited links) either if + * x is the target service or it exists a service connected to x that is used to reach the target + * + * @param tgt the target service + * @param initiator the initiator of the request + * @param x the actual service + * @param visited the set of visited tgt-x services visited + * @param lU the link relation between services + * @param p the provide relation of an initiator + * @param r the routing relation of the platform + * @tparam U the type of the service + * @return true is the service is used to reach tgt + */ + private def usedNext[U <: Service](tgt: U, initiator: Initiator, x: U, visited: Seq[(U, U)])(implicit + lU: Linked[U, U], + p: Provided[Initiator, Service], + r: RoutingRelation[(Initiator, Service, Service), Service]): Boolean = + x == tgt || + lU(x).exists(u => usedForTgt(tgt, initiator, x, u, visited)) + + /** + * A service "to" can be accessed from another service "on" to reach a target service "tgt" + * from a given initiator "ini" if "on" either + * if "on" has no predecessor and is the initiator services + * or it exists a predecessor of "on" that is routed + * Moreover if routing restriction applied (r.get((ini, tgt, on)) is defined) then "to" must be a viable option + * + * @param ini + * @param tgt + * @param on + * @param to + * @param visited + * @param lU + * @param p + * @param r + * @tparam U + * @return + */ + private def isRouted[U <: Service](ini: Initiator, tgt: U, on: U, to: U, visited: Seq[(U, U)])(implicit + lU: Linked[U, U], + p: Provided[Initiator, Service], + r: RoutingRelation[(Initiator, Service, Service), Service]): Boolean = + _memoRoute.getOrElseUpdate( + (tgt, ini, on, to), + { + if (visited.contains(on, to)) { + println(Message.cycleWarning(visited, ini, tgt)) + false + } else { + val pred = lU.inverse(on) + r.get((ini, tgt, on)) match { + case None => + (pred.isEmpty && ini.services.contains(on)) || (pred exists { x => + isRouted(ini, tgt, x, on, visited :+ ((on, to))) + }) + case Some(s) => + s.contains(to) && ((pred.isEmpty && ini.services.contains(on)) || (pred exists { x => + isRouted(ini, tgt, x, on, visited :+ ((on, to))) + })) + } + } + }) + + //visited is added to cut cycles + private def usedForTgt[U <: Service](tgt: U, ini: Initiator, from: U, to: U, visited: Seq[(U, U)])(implicit + lU: Linked[U, U], + p: Provided[Initiator, Service], + r: RoutingRelation[(Initiator, Service, Service), Service]): Boolean = { + if (visited.contains((from, to))) { + println(Message.cycleWarning(visited, ini, tgt)) + false + } else { + isRouted(ini, tgt, from, to, visited) && usedNext(tgt, ini, to, visited :+ ((from, to))) + } + } + + def apply(b: R): L +} + +object Restrict { + + def apply[L,R](using ev:Restrict[L,R]):Restrict[L,R] = ev + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * @note restrict operators is an advanced feature and should not be necessary for models + */ + trait Ops { + + extension (self:Application){ + def serviceGraph(using ev:Restrict[Map[Service, Set[Service]], Application]): Map[Service, Set[Service]] = ev(self) + def hardwareGraph(using ev:Restrict[Map[Hardware, Set[Hardware]], Application]): Map[Hardware, Set[Hardware]] = ev(self) + } + + /** + * Extension method class + * + * @param self the element on which keyword can be used + */ + extension (self: Platform) { + + /** + * PML keyword to access to the service graph of an application + * + * @param s the application + * @return its service graph + */ + def serviceGraphOf(s: Application): Map[Service, Set[Service]] = { + import self._ + val ev = implicitly[Restrict[Map[Service, Set[Service]], Application]] + ev(s) + } + + /** + * PML keyword to access to the hardware graph of the considered element + * + * @return its hardware graph + */ + def hardwareGraph(): Map[Hardware, Set[Hardware]] = + self.applications + .flatMap { + self.hardwareGraphOf + } + .groupMapReduce(_._1)(_._2)(_ ++ _) + + /** + * PML keyword to access to hardware graph used by an application + * + * @param s the application + * @return its hardware graph + */ + def hardwareGraphOf(s: Application): Map[Hardware, Set[Hardware]] = { + import self._ + val ev = implicitly[Restrict[Map[Hardware, Set[Hardware]], Application]] + ev(s) + } + + /** + * PML keyword to access to the service graph used by an application to access a target + * + * @param s the application + * @param tgt the target service + * @return its service graph + */ + def serviceGraphOf(s: Application, tgt: Service): Map[Service, Set[Service]] = { + import self._ + val ev = implicitly[Restrict[Map[Service, Set[Service]], (Application, Service)]] + ev((s, tgt)) + } + + /** + * PML keyword to access to the hardware graph used by an application to access a target + * + * @param s the application + * @param tgt the target service + * @return its service graph + */ + def hardwareGraphOf(s: Application, tgt: Service): Map[Hardware, Set[Hardware]] = { + import self._ + val ev = implicitly[Restrict[Map[Hardware, Set[Hardware]], (Application, Service)]] + ev((s, tgt)) + } + + } + + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * A restricted can be obtained from an endomorphism over services + * + * @param lS the proof that the endomorphism exists + * @param uL the proof that an application uses loads + * @param uS the proof that an application uses stores + * @param uSI the proof that an application uses initiators + * @param aR the proof that an authorize relation exists + * @param r the proof that a routing relation exists + * @param pB the proof that an initiator provide services + * @return an object building service graph for applications + */ + given (using + lS: LinkRelation[Service], + uL: Used[Application, Load], + uS: Used[Application, Store], + uSI: Used[Application, Initiator], + aR: AuthorizeRelation[Application, Service], + r: RoutingRelation[(Initiator, Service, Service), Service], + pB: Provided[Initiator, Service]): Restrict[Map[Service, Set[Service]], Application] with { + + + /** + * Check if the application uses some route between an initial service and a target service + * + * @param a the application + * @param from the initial service + * @param to the target service + * @param u the proof that application uses services of type U + * @param lU the proof that services of type U can be linked + * @tparam U the type of the service + * @return if the application uses some route between from and to + */ + def useBySW[U <: Service](a: Application, from: U, to: U)(implicit u: Used[Application, U], + lU: Linked[U, U]): Boolean = { + u(a).exists(tgt => aR(a).contains(tgt) && + a.hostingInitiators.exists(ini => usedForTgt(tgt, ini, from, to))) + } + + def used(a: Application, from: Service, to: Service): Boolean = (from,to) match { + case (fromL: Load, toL:Load) => useBySW(a, fromL, toL) + case (fromS: Store, toS: Store) => useBySW(a, fromS, toS) + case _ => false + } + + /** + * Provide the service graph of an application + * + * @param b the application + * @return its service graph + */ + def apply(b: Application): Map[Service, Set[Service]] = { + lS.edges collect { + case (from, linked) => from -> { + linked filter { + used(b, from, _) + } + } + } filter { + _._2.nonEmpty + } + } + } + + given[T] (using + lP: LinkRelation[Hardware], + pS: Provided[Hardware, Service], + restrict: Restrict[Map[Service, Set[Service]], T]): Restrict[Map[Hardware, Set[Hardware]], T] with { + def apply(b: T): Map[Hardware, Set[Hardware]] = { + val restricted = restrict(b) + //shortcut non-owner services, if k -> v and k is not owned by any HW then push v to all predecessors of k + val nonOwnedServices = restricted.keySet.collect { case b: Service if b.hardwareOwner.isEmpty => b } + val shortcut = nonOwnedServices.foldLeft(restricted)((acc, toRemove) => { + for { + toAdd <- acc.get(toRemove) + in = acc.keySet.filter(k => acc(k).contains(toRemove)) + } yield { + acc.removed(toRemove).transform((k, v) => if (in.contains(k)) v ++ toAdd else v) + } + } getOrElse acc) + val usedPLLinks = lP.edges.transform((k, v) => + v.filter(t => + shortcut.exists(ks => + k.services.contains(ks._1) && t.services.intersect(ks._2).nonEmpty + ) + ) + ).filter { + _._2.nonEmpty + } + val lostBasicEdges = shortcut.collect({ case (k: Service, v) => k -> v }).transform( + (si, v) => v.filterNot(st => + usedPLLinks.exists(kvPL => + kvPL._1.services.contains(si) && kvPL._2.flatMap(_.services).contains(st)))).filter(_._2.nonEmpty) + .toSeq.flatMap(kv => kv._2.flatMap(t => t.hardwareOwner.flatMap(tPL => kv._1.hardwareOwner.map(_ -> tPL)))) + //WARNING if a link si -> st exists where si and st have owner then a physical link is added + val completedPLLinks = lostBasicEdges.foldLeft(usedPLLinks)((acc, toAdd) => { + for {v <- acc.get(toAdd._1)} yield acc.updated(toAdd._1, v + toAdd._2) + }.getOrElse(acc + (toAdd._1 -> Set(toAdd._2)))) + completedPLLinks + } + } + + given[T <: Application | Initiator] (using + lS: LinkRelation[Service], + uSI: Used[Application, Initiator], + aR: AuthorizeRelation[Application, Service], + r: RoutingRelation[(Initiator, Service, Service), Service], + pB: Provided[Initiator, Service]): Restrict[Map[Service, Set[Service]], (T, Service)] with { + + def used(a: T, tgt: Service, from: Service, to: Service): Boolean = { + val authorized = a match { + case app: Application => aR(app).contains(tgt); + case _ => true + } + val hostingInitiators = a match { + case app:Application => app.hostingInitiators + case i:Initiator => Set(i) + } + authorized && ((tgt,to,from) match { + case (tgtL: Load,toL:Load,fromL:Load) => + hostingInitiators.exists(ini => usedForTgt(tgtL, ini, fromL, toL)) + case (tgtS: Store, toS:Store, fromS:Store) => + hostingInitiators.exists(ini => usedForTgt(tgtS, ini, fromS, toS)) + case _ => false + }) + } + + + def apply(b: (T, Service)): Map[Service, Set[Service]] = { + lS.edges collect { + case (from, linked) => from -> { + linked filter { + used(b._1, b._2, from, _) + } + } + } filter { + _._2.nonEmpty + } + } + } +} diff --git a/src/main/scala/pml/operators/Route.scala b/src/main/scala/pml/operators/Route.scala new file mode 100644 index 0000000..1f69af6 --- /dev/null +++ b/src/main/scala/pml/operators/Route.scala @@ -0,0 +1,181 @@ +package pml.operators + +import pml.model.hardware._ +import pml.model.relations.RoutingRelation +import pml.model.service._ +import pml.model.utils.Message.uselessRoutingConstraintWarning +import pml.model.utils.Owner + + +/** + * Extension methods + */ +object Route { + + /** + * Any hardware can route or forbid the transactions passing through him. + * A routing constraint can be specified as follows. + * + * If all transactions from an [[pml.model.hardware.Initiator]] (denoted initiator) to + * a [[pml.model.hardware.Target]] (denoted target) are routed by an [[pml.model.hardware.Hardware]] (denoted router) + * to one of its successor (denoted next) then + * {{{ initiator targeting target useLink router to next}}} + * If the router forbids a specific route then + * {{{initiator targeting target cannotUseLink router to next}}} + * If the router forbids all routes then + * {{{initiator targeting target isBlockedBy router}}} + * If this constraint is true for all target, then the targeting keyword can be omitted in any previous construct + * {{{initiator useLink router to next}}} + * @note the constraint is fruitful iff a service of the router is linked to a service of next + * @see route usage can be found in [[pml.examples.simpleKeystone.SimpleRoutingConfiguration]] + */ + trait Ops { + + extension(self:Initiator){ + /** + * PML keyword to specify the target used by the initiator for which a routing constraint applies + * @param target a target + * @return the partial routing constraint where the initiator target couple is specified + */ + def targeting(target: Target): SimpleRouteIdentifyRouter = + SimpleRouteIdentifyRouter(self, Seq(target)) + + /** + * PML keyword to specify the set of targets used by the initiator for which a routing constraint applies + * @param target a set of targets + * @return the partial routing constraint where the initiator target couples are specified + */ + def targeting(target: Iterable[Target]): SimpleRouteIdentifyRouter = + SimpleRouteIdentifyRouter(self, target) + + /** + * PML keyword to specify the hardware routing the transactions + * @param router an hardware + * @return a partial routing constraint where the initiator, the targets and the router are specified + */ + def useLink(router: Hardware)(implicit owner: Owner): SimpleRouterIdentifyNext = + SimpleRouterIdentifyNext(self, Target.all, router, forbid = false) + + /** + * PML keyword to specify the router blocking the route + * @param router an hardware + * @return a partial routing constraint where the initiator, the targets and the router are specified + */ + def cannotUseLink(router: Hardware)(implicit owner: Owner): SimpleRouterIdentifyNext = + SimpleRouterIdentifyNext(self, Target.all, router, forbid = true) + + } + + /** + * Partial routing constraint where the initiator target couples are specified + * @param a the initiator + * @param targets the set of targets + */ + case class SimpleRouteIdentifyRouter(a: Initiator, targets: Iterable[Target]) { + + /** + * PML keyword to specify the hardware routing the transactions + * @param router an hardware + * @return a partial routing constraint where the initiator, the targets and the router are specified + */ + def useLink(router: Hardware): SimpleRouterIdentifyNext = + SimpleRouterIdentifyNext(a, targets, router, forbid = false) + + /** + * PML keyword to specify the router blocking the route + * @param router an hardware + * @return a partial routing constraint where the initiator, the targets and the router are specified + */ + def cannotUseLink(router: Hardware): SimpleRouterIdentifyNext = + SimpleRouterIdentifyNext(a, targets, router, forbid = true) + + /** + * PML keyword to specify the router blocking all routes + * @param router an hardware + */ + def blockedBy(router: Hardware)(implicit + p: Provided[Hardware, Service], + l: Linked[Service, Service], + r:RoutingRelation[(Initiator, Service, Service), Service]): Unit = { + for {t <- targets + tL <- t.loads + rL <- router.loads + } yield { + update(tL, rL, l(rL)) + } + + for {t <- targets + tS <- t.stores + rS <- router.stores + } yield { + update(tS, rS, l(rS)) + } + } + + private def update[T <: Service](t: T, on: T, next: Set[T])(implicit + l: Linked[T, T], + r:RoutingRelation[(Initiator, Service, Service), Service]): Unit = + r.get((a, t, on)) match { + case Some(_) => + r.remove((a, t, on), next) + case None => + r.add((a, t, on), l(on) -- next) + } + } + + /** + * Partial routing constraint where the initiator target couples, the router and the type of routing + * constraint are specified + * @param a the initiator + * @param targets the set of targets + * @param router the router + * @param forbid if it is a blocking or routing constraint + */ + case class SimpleRouterIdentifyNext(a: Initiator, targets: Iterable[Target], router: Hardware, forbid:Boolean) { + + /** + * PML keyword to specify the link from [[router]] that is routed or blocked + * @param next the component linked to [[router]] + * @param p the proof that hardware provide services + * @param l the proof that services can be linked + * @param r the proof that a routing relation exists + */ + def to(next: Hardware)(implicit + p: Provided[Hardware, Service], + l: Linked[Service, Service], + r:RoutingRelation[(Initiator, Service, Service), Service]): Unit = { + if (!next.services.exists(s => router.services.exists(s2 => l(s2).contains(s)))) + println(uselessRoutingConstraintWarning(router, next)) + else { + for {t <- targets + tL <- t.loads + rL <- router.loads + } yield { + update(tL, rL, next.loads) + } + + for {t <- targets + tS <- t.stores + rS <- router.stores + } yield { + update(tS, rS, next.stores) + } + } + } + + private def update[T <: Service](t: T, on: T, next: Set[T])(implicit + l: Linked[T, T], + r:RoutingRelation[(Initiator, Service, Service), Service]): Unit = { + r.get((a, t, on)) match { + case Some(_) if forbid => + r.remove((a, t, on), next) + case None if forbid => + r.add((a, t, on), l(on) -- next) + case _ => + r.remove((a, t, on)) + r.add((a, t, on), next) + } + } + } + } +} diff --git a/src/main/scala/pml/operators/Use.scala b/src/main/scala/pml/operators/Use.scala new file mode 100644 index 0000000..86cec39 --- /dev/null +++ b/src/main/scala/pml/operators/Use.scala @@ -0,0 +1,178 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.operators + +import pml.model.hardware.{Initiator, Target} +import pml.model.relations.{AuthorizeRelation, UseRelation} +import pml.model.service.{Load, Service, Store} +import pml.model.software.{Application, Data} + +/** + * Base trait for use operator + * + * @tparam L the left type + * @tparam R the right type + */ +trait Use[L, R] { + def apply(l: L, r: R): Unit +} + +object Use { + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element l can use an element r then the following operators can be used + * + * l (e.g. an [[pml.model.software.Application]]) is hosted by an element r (e.g. a [[pml.model.hardware.Initiator]]) + * {{{ l hostedBy r}}} + * l (e.g. an [[pml.model.software.Application]]) reads an element r (e.g. a [[pml.model.software.Data]]) + * {{{ l read r}}} + * l writes an element r + * {{{ l write r}}} + * + * @note [[pml.model.software]] uses [[pml.model.hardware.Hardware]] through hostedBy keyword + * and [[pml.model.software.Application]] uses [[pml.model.software.Data]] or directly [[pml.model.hardware.Target]] + * through read and write keywords + * @see usage can be found in [[pml.examples.simpleKeystone.SimpleSoftwareAllocation]] + */ + trait Ops { + + /** + * Extension method class + */ + extension[L <: Application | Initiator] (self: L) { + + private def use[B](b: B)(using ev: Use[L, B]): (L, B) = { + ev(self, b) + self -> b + } + + private def use[B](b: Set[B])(using ev: Use[L, B]): Set[(L, B)] = b.map(x => use(x)) + + /** + * The PML keyword specify that self reads something + * + * @param b the element to read + * @param p that the element provide load services + * @param ev the proof that an application can use load services + * @tparam B the type of b + * @return the link + */ + def read[B](b: B)(using p: Provided[B, Load], ev: Use[L, Load]): Set[(L, Load)] = use(b.loads) + + /** + * The PML keyword specify that self uses some load services + * + * @param b the set of services + * @param ev the proof that an application can use load services + * @return the link + */ + def read(b: Set[Service])(using ev: Use[L, Load]): Set[(L, Load)] = use(b.collect { case l: Load => l }) + + /** + * The PML keyword specify that self reads something + * + * @param b the set of elements to read + * @param p that the element provide load services + * @param ev the proof that an application can use load services + * @tparam B the type of b + * @return the link + */ + def read[B](b: Set[B])(using p: Provided[B, Load], ev: Use[L, Load]): Set[(L, Load)] = use(b.loads) + + /** + * The PML keyword specify that self writes something + * + * @param b the element to write + * @param p that the element provide store services + * @param ev the proof that an application can use store services + * @tparam B the type of b + * @return the link + */ + def write[B](b: B)(using p: Provided[B, Store], ev: Use[L, Store]): Set[(L, Store)] = use(b.stores) + + /** + * The PML keyword specify that self uses some store services + * + * @param b the set of services + * @param ev the proof that an application can use store services + * @return the link + */ + def write(b: Set[Service])(using ev: Use[L, Store]): Set[(L, Store)] = use(b.collect { case l: Store => l }) + } + + extension [L<: Application](self:L) { + /** + * The PML keyword to allocate self on an initiator + * + * @param b the initiator + * @param ev the proof that self can use an initiator + * @return the link + */ + def hostedBy(b: Initiator)(using ev: Use[L, Initiator]): (L, Initiator) = { + ev(self, b) + self -> b + } + } + + + /** + * Extension method class + * + * @param self the element on which keyword can be used + * @tparam L the concrete type of the element + */ + extension[L <: Data] (self: L) { + + /** + * The PML keyword to allocate self on an target + * + * @param b the target + * @param ev the proof that self can use an target + * @return the link + */ + def hostedBy(b: Target)(using ev: Use[Data, Target]): (Data, Target) = { + ev(self, b) + self -> b + } + } + + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + given [I <: Initiator, S<:Service] (using l:UseRelation[I,Service]): Use[I,S] with { + def apply(a: I, b: S): Unit = l.add(a,b) + } + + given [A <: Application, S<:Service] (using l: UseRelation[Application, Service], aR: AuthorizeRelation[Application, Service]): Use[A, S] with { + def apply(a: A, b: S): Unit = { + l.add(a, b) + aR.add(a, b) + } + } + + given [AD<:Application | Data, H] (using l: UseRelation[AD, H]): Use[AD, H] with { + def apply(a: AD, b: H): Unit = l.add(a, b) + } +} \ No newline at end of file diff --git a/src/main/scala/pml/operators/Used.scala b/src/main/scala/pml/operators/Used.scala new file mode 100644 index 0000000..26c98ac --- /dev/null +++ b/src/main/scala/pml/operators/Used.scala @@ -0,0 +1,317 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package pml.operators + +import pml.model.hardware.{Initiator, Platform, Target} +import pml.model.relations.UseRelation +import pml.model.service.{Load, Service, Store} +import pml.model.software.{Application, Data} +import pml.model.utils.Message._ +import scalaz.Memo.immutableHashMapMemo +import views.interference.model.specification.InterferenceSpecification.{Path, PhysicalTransaction} +import pml.operators._ +import scala.reflect._ + +/** + * Base trait for used operator + * + * @tparam L the left type + * @tparam R the right type + */ +trait Used[L, R] { + def apply(a: L): Set[R] +} + +object Used { + + /** ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element l can use an element r then the following operators can be used + * + * l (e.g. a [[pml.model.hardware.Target]]) can provide the hosted [[pml.model.software.Data]] + * {{{l.hostedData }}} + * l (e.g. a [[pml.model.hardware.Platform]]) can provide the hosted [[pml.model.software.Application]] + * {{{l.applications }}} + * l (e.g. a [[pml.model.hardware.Platform]]) can provide + * the [[views.interference.model.specification.InterferenceSpecification.PhysicalTransaction]] that can occur + * {{{l.usedTransactions}}} + * l (e.g. a [[pml.model.hardware.Platform]]) can provide the multi-path + * [[views.interference.model.specification.InterferenceSpecification.PhysicalTransaction]] that can occur + * {{{l.multiPathsTransactions}}} + * + * @see usage can be found in [[pml.examples.simpleKeystone.SimpleKeystoneExport]] + */ + trait Ops { + + /** + * Extension method class + */ + extension[L] (self: L) { + + /** + * PML keyword to access to elements used by self + * + * @param ev the proof that self can use elements of type B + * @tparam B the type of used elements + * @return the set of used elements + */ + def used[B]()(using ev: Used[L, B]): Set[B] = ev(self) + } + + /** + * Extension method class + */ + extension (self: Target) { + + /** + * PML keyword to access to data hosted by self + * + * @param ev the proof that self can host data + * @return + */ + def hostedData(using ev: Used[Target, Data]): Set[Data] = ev(self) + } + + /** + * Extension method class + */ + extension[L <: Platform] (self: L) { + + /** + * PML keyword to access to applications hosted by self + * + * @return the set of hosted applications + */ + def applications: Set[Application] = Application.allDirect(self.currentOwner) + + /** + * PML keyword to access to physical transactions used by self + * + * @param ev the proof that self can use transactions + * @return the set of used physical transactions + */ + def usedTransactions(using ev: Used[L, PhysicalTransaction]): Set[PhysicalTransaction] = ev(self) + + /** + * PML keyword to access to multi-path physical transactions used by self + * + * @param ev the proof that self can use transactions + * @return the set of multi path used physical transactions + */ + def multiPathsTransactions(using ev: Used[L, PhysicalTransaction]): Set[Set[PhysicalTransaction]] = { + Used.getMultiPaths(usedTransactions).values.toSet + } + } + + /** + * Extension method class + */ + extension (self: Data) { + + /** + * PML keyword to access to the targets hosting self + * + * @param ev the proof that self can be hosted by targets + * @return the set of targets + */ + def hostingTargets(using ev: Used[Data, Target]): Set[Target] = ev(self) + } + + /** + * Extension method class + */ + extension [L <: Application | Initiator](self: L) { + + /** + * PML keyword to access to the load services used by self + * + * @param ev the proof that self can use load services + * @return the used loads + */ + def targetLoads(using ev: Used[L, Load]): Set[Load] = ev(self) + + /** + * PML keyword to access to the store services used by self + * + * @param ev the proof that self can use store services + * @return the used stores + */ + def targetStores(using ev: Used[L, Store]): Set[Store] = ev(self) + + /** + * PML keyword to access to the services used by self + * + * @param ev the proof that self can use services + * @return the used services + */ + def targetService(using ev: Used[L, Service]): Set[Service] = ev(self) + + /** + * PML keyword to access to the initiator hosting self + * + * @param ev the proof that self can be hosted by initiators + * @return the hosting initiators + */ + def hostingInitiators(using ev: Used[L, Initiator]): Set[Initiator] = ev(self) + } + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + // basic cases + given[T, U] (using l: UseRelation[T, U]): Used[T, U] with { + def apply(a: T): Set[U] = l(a) + } + + + given[T, U] (using l: UseRelation[T, U]): Used[U, T] with { + def apply(a: U): Set[T] = l.inverse(a) + } + + + // derivations + given[P <: Platform : Typeable]: Used[P, PhysicalTransaction] with { + def apply(a: P): Set[PhysicalTransaction] = { + import a._ + + def usedTransactionsBy[U <: Initiator | Application](x: U)(using + u:Used[U,Service], + r:Restrict[Map[Service, Set[Service]],(U,Service)], + typeable: Typeable[U & Initiator]): Set[Path[Service]] = { + val allTargets = x.targetService + + val serviceGraph = allTargets.groupMapReduce(s => s)(s => r((x, s)))((l, r) => l ++ r) + + val fromServices = x match{ + case app: Application => app.hostingInitiators.flatMap(_.services) + case ini: Initiator => ini.services + } + + val paths = serviceGraph.transform((_, graph) => fromServices flatMap { from => pathsIn(from, graph) }) + + val result = paths.values.flatten.toSet + x match { + case sw: Application => + checkTransactions(result, allTargets, Some(sw)).foreach(println) + checkApplicationAllocation(sw) + case _:Initiator => + checkTransactions(result, allTargets, None).foreach(println) + } + result + } + + val result = a.applications.flatMap(usedTransactionsBy) ++ a.initiators.flatMap(usedTransactionsBy) + result + } + } + + given [AI <: Application | Initiator, S <: Service : Typeable] (using l: Used[AI, Service]): Used[AI, S] with { + def apply(a: AI): Set[S] = l(a) collect { case s : S => s } + } + + given [A <:Application, I<:Initiator : Typeable] (using l:Used[Application,Initiator]): Used[A,I] with { + def apply(a: A): Set[I] = l(a) collect { case s : I => s } + } + + /** ------------------------------------------------------------------------------------------------------------------ + * UTIL METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + private def checkApplicationAllocation(a: Application)(implicit uSWSrv: Used[Application, Service], + uAS: Used[Application, Initiator], + pSB: Provided[Initiator, Service]): Set[String] = { + val noTarget = + if (a.targetService.isEmpty) + Set(applicationNotUsingServicesWarning(a)) + else Set.empty + val noExecutor = a match { + case application: Application if application.hostingInitiators.isEmpty => + Set(applicationNotAllocatedWarning(application)) + case _ => + Set.empty + } + val noBasics = a.hostingInitiators + .filter(s => s.services.isEmpty) + .map(noServiceInitiatorWarning(a, _)) + + noTarget ++ noBasics ++ noExecutor + } + + /** + * Compute all the path from a given element to the leaf services + * This methods handle cyclic graphs by simply cutting the loop when traversing it + * + * @param from the initial service + * @param graph the edges of the graph + * @tparam A the type of the parent nodes + * @tparam B the type of the son nodes + * @return all the possible paths + */ + private def pathsIn[A, B <: A](from: A, graph: Map[A, Set[B]]): Set[Path[A]] = { + + /** + * This function value compute the path from a node of the graph to its leaf nodes (first element of the Pair). + * A set of visited nodes is also provided (second element of the Pair) to cut cycles + * The result are memoized to avoid multiple computation of the paths + */ + lazy val _paths: ((A, Set[A])) => Set[Path[A]] = immutableHashMapMemo { + s => + if (s._2.contains(s._1)) { + println(cyclicGraphWarning) + Set(Nil) + } + else if (s._2.contains(s._1) || !graph.contains(s._1) || graph(s._1).isEmpty) + Set(Nil) + else { + for { + next <- graph(s._1) + path <- _paths(next, s._2 + s._1) + } yield + next +: path + } + } + + //remove empty paths (i.e. from is not connected to anyone in the graph) and add from as path head + _paths((from, Set.empty)) collect { + case p if p.nonEmpty => from +: p + } + } + + def checkImpossible(s: Set[PhysicalTransaction], target: Set[Service] = Set.empty, a: Option[Application] = None): Set[String] = { + target + .filterNot(t => s.exists(_.last == t)) + .map(impossibleRouteWarning(_, a)) + } + + private def getMultiPaths(s: Set[PhysicalTransaction]): Map[(Service, Service), Set[PhysicalTransaction]] = + s.groupBy(t => (t.head, t.last)) + .filter(_._2.size >= 2) + + def checkMultiPaths(s: Set[PhysicalTransaction]): Set[String] = + getMultiPaths(s) + .map(kv => multiPathRouteWarning(kv._1._1, kv._1._2, kv._2)).toSet + + + private def checkTransactions(s: Set[PhysicalTransaction], target: Set[Service] = Set.empty, a: Option[Application] = None): Set[String] = + checkImpossible(s, target, a) ++ checkMultiPaths(s) +} diff --git a/src/main/scala/pml/operators/package.scala b/src/main/scala/pml/operators/package.scala new file mode 100644 index 0000000..73c3137 --- /dev/null +++ b/src/main/scala/pml/operators/package.scala @@ -0,0 +1,28 @@ +package pml +/** + * Package containing all the extension methods provided by operators + * + * @note This package should be imported in all pml models as so + * {{{ + * import pml.operators._ + * }}} + * @see [[Link.Ops]] for link (e.g. link/unlink) keywords + * @see [[Linked.Ops]] for linked (e.g. linked/reverse) keywords + * @see [[Deactivate.Ops]] for deactivate (e.g. deactivate) keywords + * @see [[Provided.Ops]] for provide (e.g. services/loads/stores) keywords + * @see [[Use.Ops]] for use (e.g. use/hostedBy) keywords + * @see [[Used.Ops]] for used (e.g. used/targetLoads) keywords + * @see [[Merge.Ops]] for and (e.g. and) keywords + * @see [[Restrict.Ops]] for restrict (e.g. restrictedTo) keywords + * @see [[Route.Ops]] for route (e.g. useLink/cannotUseLink) keywords + */ +package object operators extends Link.Ops + with Linked.Ops + with Deactivate.Ops + with Provided.Ops + with Use.Ops + with Used.Ops + with Merge.Ops + with Restrict.Ops + with Route.Ops + with AsTransaction.Ops diff --git a/src/main/scala/pml/package.scala b/src/main/scala/pml/package.scala new file mode 100644 index 0000000..b4c4b7e --- /dev/null +++ b/src/main/scala/pml/package.scala @@ -0,0 +1,86 @@ + +/** + * Package containing all general modelling features of PML + * @see For basic classes used to model a platform see [[pml.model]] + * @see For operators used to manipulate the classes see [[pml.operators]] + * @see For exporters see [[pml.exporters]] + * @see For examples on operator and class usage see [[pml.examples]] and + * the related documentation in src/main/doc-resources/pml/examples + * @groupname software_class Software PML nodes + * @groupprio software_class 4 + * @groupname service_class Service PML nodes + * @groupprio service_class 4 + * @groupname target_class Target PML nodes + * @groupprio target_class 4 + * @groupname hardware_class Hardware PML nodes + * @groupprio hardware_class 4 + * @groupname initiator_class Initiator PML nodes + * @groupprio initiator_class 4 + * @groupname hierarchical_class Hierarchical PML nodes + * @groupprio hierarchical_class 4 + * @groupname transporter_class Transporter PML nodes + * @groupprio transporter_class 4 + * @groupname builder PML node builders + * @groupprio builder 4 + * @groupname transaction_class Transaction classes + * @groupprio transaction_class 4 + * @groupname scenario_class Scenario classes + * @groupprio scenario_class 4 + * @groupname transaction_operation Transaction operators + * @groupprio transaction_operation 2 + * @groupname scenario_operation Scenario operators + * @groupprio scenario_operation 2 + * @groupname user_transaction_relation Used user transaction relations + * @groupprio user_transaction_relation 3 + * @groupname user_scenario_relation Used user scenario relations + * @groupprio user_scenario_relation 3 + * @groupname transaction_def User transaction definition + * @groupprio transaction_def 0 + * @groupname scenario_def User scenario definition + * @groupprio scenario_def 0 + * @groupname identifier Identifiers + * @groupprio identifier 3 + * @groupname target Target components + * @groupprio target 0 + * @groupname transporter Transporter components + * @groupprio transporter 0 + * @groupname initiator Initiator components + * @groupprio initiator 0 + * @groupname composite Composite components + * @groupprio composite 0 + * @groupname composite_def Composite definition + * @groupprio composite_def 1 + * @groupname application Applications + * @groupprio application 0 + * @groupname data Data + * @groupprio data 0 + * @groupname load Load services + * @groupprio load 0 + * @groupname store Store services + * @groupprio store 0 + * @groupname component_access Internal component accessors + * @groupprio component_access 5 + * @groupname printer_function Print functions + * @groupprio printer_function 8 + * @groupname route_relation Route relations + * @groupprio route_relation 3 + * @groupname platform_def Platform definition + * @groupprio platform_def 0 + * @groupname embedFunctions Contained node functions + * @groupprio embedFunctions 5 + * @groupname utilFun Utility functions + * @groupprio utilFun 5 + * @groupname publicConstructor Public constructors + * @groupprio publicConstructor 5 + * @groupname transaction Transaction relations + * @groupprio transaction 0 + * @groupname auth_relation Authorize relation + * @groupprio auth_relation 3 + * @groupname link_relation Link relations + * @groupprio link_relation 3 + * @groupname provide_relation Provide relations + * @groupprio provide_relation 3 + * @groupname use_relation Use relations + * @groupprio use_relation 3 + */ +package object pml \ No newline at end of file diff --git a/src/main/scala/views/dependability/executor/Scheduler.scala b/src/main/scala/views/dependability/executor/Scheduler.scala new file mode 100644 index 0000000..b45a2c2 --- /dev/null +++ b/src/main/scala/views/dependability/executor/Scheduler.scala @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.executor + +import views.dependability.model.ConcreteEvent +import views.dependability.model.Direction.{Degradation, Reparation} + +trait Scheduler { + def schedule(fireable:Iterable[ConcreteEvent]):Iterable[ConcreteEvent] +} + +object WorstCaseSchedule extends Scheduler { + def schedule(fireable: Iterable[ConcreteEvent]): Iterable[ConcreteEvent] = { + fireable.filter(e => e.owner.direction(e).contains(Degradation)) match { + case s if s.isEmpty => + fireable.filter(e => e.owner.direction(e).contains(Reparation)) + case s => s + } + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/executor/Simulator.scala b/src/main/scala/views/dependability/executor/Simulator.scala new file mode 100644 index 0000000..84a5aaa --- /dev/null +++ b/src/main/scala/views/dependability/executor/Simulator.scala @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.executor + +import views.dependability.model._ + +import scala.collection.mutable + +object Simulator { + + var time: Int = 0 + + val events: mutable.Set[ConcreteEvent] = mutable.Set.empty + + def addEvent(e: ConcreteEvent): Unit = events += e + + def fireable: Set[Event] = events.filter(e => e.owner.fireable(e).isDefined).toSet + + def fireEvent(e: Event): Unit = { + e match { + case sync@SynchroEvent(_, synchronizedEvents) => + if (synchronizedEvents.nonEmpty) { + Synchronize.fireSynchro(sync) + } + case concreteEvent: ConcreteEvent => + Synchronize.findSynchroOf(concreteEvent) match { + case Some(sync) => Synchronize.fireSynchro(sync) + case None => concreteEvent.owner.fire(concreteEvent) + } + } + + val immediate = fireable.collect { case e@DetermisticEvent(_, _, 0) => e } + if (immediate.isEmpty) { + val det = fireable.collect { case e@DetermisticEvent(_, _, i) if i != 0 => e } + if (det.nonEmpty) { + val min = det.minBy(_.delay).delay + det.filter(_.delay == min) match { + case minDet if minDet.size == 1 => + time += minDet.head.delay + fireEvent(minDet.head) + case minDet => + // throw new Exception(s"choice between instantaneous events $minDet") + val synchroEvent = Synchronize.addSynchro(WorstCaseSchedule.schedule(minDet).toSet) + fireEvent(synchroEvent) + Synchronize.removeSynchro(synchroEvent) + } + } + } + else if (immediate.size == 1) + fireEvent(immediate.head) + else { + // throw new Exception(s"choice between instantaneous events $s") + val synchroEvent = SynchroEvent(Symbol("schedule"), WorstCaseSchedule.schedule(immediate).toSet) + fireEvent(synchroEvent) + Synchronize.removeSynchro(synchroEvent) + } + } +} diff --git a/src/main/scala/views/dependability/executor/Synchronize.scala b/src/main/scala/views/dependability/executor/Synchronize.scala new file mode 100644 index 0000000..15dfaac --- /dev/null +++ b/src/main/scala/views/dependability/executor/Synchronize.scala @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.executor + +import views.dependability.model.{ConcreteEvent, Event, SynchroEvent} + +import scala.collection.mutable + +object Synchronize { + + private var counter = -1 + + def freshSyncName():Symbol = { + counter += 1 + Symbol(s"synchro$counter") + } + + private val synchronizations : mutable.Set[SynchroEvent] = mutable.Set.empty[SynchroEvent] + + def findSynchroOf(e:ConcreteEvent) : Option[SynchroEvent] ={ + synchronizations.find(t => t.synchronizedEvents.contains(e)) + } + + def addSynchro(s:Set[Event]) : SynchroEvent = { + //WARNING synchro operation is transitive ie sync(s1,{a,b}), synch(s2,{b,c} => synch(s3,{a,b,c}) + (for (sync <- synchronizations.find(s2 => s2.synchronizedEvents.intersect(s).nonEmpty)) yield { + val newSync = sync.copy(synchronizedEvents = sync.synchronizedEvents.union(s)) + synchronizations -= sync + synchronizations += newSync + newSync + }) getOrElse { + val r = SynchroEvent(freshSyncName(),s) + synchronizations += r + r + } + } + + def removeSynchro(s:SynchroEvent) : Unit = synchronizations -= s + + def isFireable(s:SynchroEvent) : Boolean = + s.synchronizedEvents.foldLeft(true)((acc,e) => e match { + case concrete:ConcreteEvent => acc && concrete.owner.fireable(concrete).nonEmpty + case synchroEvent:SynchroEvent => acc && isFireable(synchroEvent) + }) + + def fireSynchro(s:SynchroEvent) : Unit = { + if(isFireable(s)){ + s.synchronizedEvents.foreach{ + case concrete:ConcreteEvent => + concrete.owner.engage(concrete) + case synchroEvent:SynchroEvent => + fireSynchro(synchroEvent) + } + s.synchronizedEvents.foreach{ + case concrete:ConcreteEvent => + concrete.owner.update() + case synchroEvent:SynchroEvent => + fireSynchro(synchroEvent) + } + } else { + throw new Exception(s"synchro $s cannot be triggered") + } + } +} diff --git a/src/main/scala/views/dependability/exporters/AutomatonCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/AutomatonCeciliaExporter.scala new file mode 100644 index 0000000..3a5ea74 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/AutomatonCeciliaExporter.scala @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.exporters.GenericImage._ +import views.dependability.exporters.PhylogFolder.automatonFamilyFolder +import views.dependability.model.{Direction, Fire, InputFMAutomaton, SimpleFMAutomaton} +import views.dependability.operators._ + +trait AutomatonCeciliaExporter { + self: TypeCeciliaExporter => + implicit def simpleFMAutomatonIsExportable[T: IsCriticityOrdering : IsFinite]: Aux[SimpleFMAutomaton[T], ComponentModel] = new CeciliaExporter[SimpleFMAutomaton[T]] { + type R = ComponentModel + + def toCecilia(x: SimpleFMAutomaton[T]): ComponentModel = { + val events = + allOf[T].flatMap(to => allOf[T].collect({ + case from if from < to => + StochastiqueEventModel(x.eventMap(to.name).name) + })).toList.distinct + val transitions = + allOf[T].flatMap(to => allOf[T].collect({ + case from if from < to => + s"s = ${from.name.name} |- ${x.eventMap(to.name).name.name} -> s := ${to.name.name};" + })).mkString("trans\n", "\n", "\n") + val assertions = + s"""assert + |o = s; + |icone = (if o = ${x.initialState.name.name} then 1 else 2);""".stripMargin + + ComponentModel( + Symbol("inputIndepFMAutomaton"), + automatonFamilyFolder, + sourceBlueCircle, + sourceGreenCircle :: sourceRedCross :: Nil, + List(Flow(Symbol("o"), typeModel[T], Out)), + events, + List(State(Symbol("s"), typeModel[T], x.initialState.name.name)), + transitions + assertions + ) + } + } + + implicit def inputFMAutomatonIsExportable[T: IsCriticityOrdering : IsFinite : IsShadowOrdering]: Aux[InputFMAutomaton[T], ComponentModel] = new CeciliaExporter[InputFMAutomaton[T]] { + type R = ComponentModel + + def toCecilia(x: InputFMAutomaton[T]): ComponentModel = { + val in = Flow(x.in.id.name, typeModel[T], In) + val fire = Flow(Symbol("fire"), typeModel[Fire.Value],In) + val o = Flow(x.o.id.name, typeModel[T], Out) + val state = State(Symbol("s"), typeModel[T], x.initialState.name.name) + val nextState = Flow(Symbol("nextState"), typeModel[T], Local) + val nextStateAssertion = allOf[T].flatMap(to => allOf[T].collect({ + case from if to.inputShadow(from) != from => + s"$in = ${to.name.name} and s = ${from.name.name} : ${to.name.name}," + })).mkString(s"$nextState = case {\n","\n",s"\nelse $state};") + val direction = Flow(Symbol("direction"), typeModel[Direction.Value], Out) + val directionAssertion = allOf[T].flatMap(from => allOf[T].collect { + case to if from < to => + s"$state = $from and $nextState = $to : ${Direction.Degradation}," + case to if from > to => + s"$state = $from and $nextState = $to : ${Direction.Reparation}," + }).mkString(s"$direction = case {\n","\n",s"\nelse ${Direction.Constant}};") + val events = + allOf[T].flatMap(to => allOf[T].collect({ + case from if from < to => + StochastiqueEventModel(x.eventMap(to.name).name) + })).toList.distinct :+ DeterministicEventModel(x.epsilon.name) + val transitionsFailure = + allOf[T].flatMap(to => allOf[T].collect({ + case from if from < to => + s"s = ${from.name.name} |- ${x.eventMap(to.name).name.name} -> s := ${to.name.name};" + })).mkString("\n") + ComponentModel( + Symbol("inputDepFMAutomaton"), + automatonFamilyFolder, + functionBlueCircle, + functionGreenCircle :: functionRedCross :: Nil, + in :: o :: nextState :: direction :: fire :: Nil, + events, + state :: Nil, + s"""trans + |//if an update is asked by the scheduler then update the state + |$fire = ${Fire.Apply} |- ${x.epsilon.name.name} -> $state := $nextState; + | + |//if updates must be performed by other components then hold the current state (so do nothing) + |$fire = ${Fire.Wait} |- ${x.epsilon.name.name} -> ; + | + |//apply failure mode as soon as the event occurs, keeping in mind the SHADOW relation over failure modes + |$transitionsFailure + | + |assert + |$o = $state; + | + |$nextStateAssertion + | + |$directionAssertion + | + |icone = (if $o = ${x.initialState.name.name} then 1 else 2); + |""".stripMargin + ) + } + } +} diff --git a/src/main/scala/views/dependability/exporters/BasicOperationCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/BasicOperationCeciliaExporter.scala new file mode 100644 index 0000000..022b65a --- /dev/null +++ b/src/main/scala/views/dependability/exporters/BasicOperationCeciliaExporter.scala @@ -0,0 +1,313 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.model.{Component, Direction, Fire, Variable, VariableId, System => DepSystem} +import views.dependability.operators._ + +trait BasicOperationCeciliaExporter { + self:TypeCeciliaExporter => + + def pathName(x: DepSystem, c: Component): List[String] = x.context.componentOwner.get(c) match { + case Some(up) => pathName(x, up) :+ c.id.toString + case None => c.id.toString :: Nil + } + + def variablePathName(x: DepSystem, v: Variable[_]): String = { + s"${pathName(x, x.context.portOwner(v.id)).mkString(".")}.${v}" + } + + def mkVariableName(v: VariableId, tyype: CeciliaType, orientation: Orientation, arity: Int): List[Flow] = arity match { + case 0 => Nil + case 1 => Flow(v.name, tyype, orientation) :: Nil + case n => (0 until n).map(i => Flow(Symbol(s"$v$i"), tyype, orientation)).toList + } + + def configurableCstModel[T: IsFinite : IsCriticityOrdering]: ComponentModel = { + val tyype = typeModel[T] + val output = Flow(Symbol("o"), tyype, Out) + val state = State(Symbol("s"), tyype, min[T].name.name) + ComponentModel(Symbol("constant"), + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.sourceBlueCircle, + Nil, + output :: Nil, + Nil, + state :: Nil, + s"assert\n$output = $state;" + ) + } + + def configurableCstModel[K: IsFinite, V: IsFinite : IsCriticityOrdering]: ComponentModel = { + val tyype = typeModel[K, V] + val output = Flow(Symbol("o"), tyype, Out) + val states = tyype.fields.map(field => State(field.name, field.tyype, min[V].name.name)) + ComponentModel(Symbol("constant"), + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.sourceBlueCircle, + Nil, + output :: Nil, + Nil, + states, + s"assert\n${tyype.fields.map(field => s"$output^$field = $field;").mkString("\n")}" + ) + } + + def equalModel[T: IsFinite]: ComponentModel = { + val tyype = typeModel[T] + ComponentModel( + Symbol("equal"), + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.equalBlue, + GenericImage.equalGreen :: GenericImage.equalRed :: Nil, + Flow(Symbol("i1"), tyype, In) :: Flow(Symbol("i2"), tyype, In) :: Flow(Symbol("o"), CeciliaBoolean, Out) :: Nil, + Nil, + Nil, + s"""assert + |o = (i1 = i2); + |icone = (if o then 1 else 2); + """.stripMargin) + } + + def switchModel[T: IsFinite : IsCriticityOrdering]: ComponentModel = { + val tyype = typeModel[T] + ComponentModel( + Symbol("switch"), + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.selectBlue, + GenericImage.select1Green :: GenericImage.select1Red :: GenericImage.select2Green :: GenericImage.select2Red :: Nil, + Flow(Symbol("select1"), CeciliaBoolean, In) :: Flow(Symbol("i1"), tyype, In) :: Flow(Symbol("i2"), tyype, In) :: Flow(Symbol("o"), tyype, Out) :: Nil, + Nil, + Nil, + s"""assert + |o = (if select1 then i1 else i2); + |icone = case { + | select1 and o = ${min[T].name.name} : 1, + | select1 : 2, + | not select1 and o = ${min[T].name.name} : 3, + | else 4 + |}; + """.stripMargin) + } + + def preModel[T: IsFinite : IsCriticityOrdering]: ComponentModel = { + val tyype = typeModel[T] + ComponentModel( + Symbol("pre"), + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.preBlue, + GenericImage.preGreen :: GenericImage.preRed :: Nil, + Flow(Symbol("currentValue"), tyype, In) :: Flow(Symbol("preValue"), tyype, Out) :: Nil, + DeterministicEventModel(Symbol("epsilon")) :: Nil, + State(Symbol("storedValue"), tyype, min[T].name.name) :: Nil, + s"""trans + |currentValue != storedValue |- epsilon -> storedValue:=currentValue; + | + |assert + |preValue = storedValue; + | + |icone = (if storedValue = ${min[T].name.name} then 1 else 2); + """.stripMargin) + } + + private val _bestMemo = collection.mutable.HashMap.empty[Set[String],(String,Seq[String],SubComponent)] + + def mkBestSub[T: IsFinite : IsCriticityOrdering](l:Set[String]): (String,Seq[String],SubComponent) = _bestMemo.getOrElseUpdate(l,{ + val model = BestModelHelper[T](l.size) + val uuid = _bestMemo.keys.count(p => p.size == l.size) + val subName = s"${model.name.name}$uuid" + val worstAssertions = model.inputs.map{f => s"$subName.$f"} zip l map (p => s"${p._1} = ${p._2};") + (s"$subName.${model.result}",worstAssertions,SubComponent(Symbol(subName),model.model)) + }) + + private case class BestModelHelper[T: IsFinite : IsCriticityOrdering](size: Int) { + val tyype: EnumeratedType = typeModel[T] + val inputs: List[Flow] = (1 to size).map(i => Flow(Symbol(s"i$i"), tyype, In)).toList + val operatorModel: OperatorModel = minOperator[T](size) + val result: Flow = Flow(Symbol("o"), tyype,Out) + val name : Symbol = Symbol(s"best$size${nameOf[T].name}") + val model: ComponentModel = ComponentModel( + name, + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.maxBlue, + GenericImage.maxGreen :: GenericImage.maxRed :: Nil, + inputs :+ result, + Nil, + Nil, + s"""assert + |o = ${operatorModel.output}${inputs.mkString("(", ",", ")")}; + |icone = (if o = ${min[T].name.name} then 1 else 2); + """.stripMargin) + } + + def minOperator[T: IsFinite : IsCriticityOrdering](size: Int): OperatorModel = { + val tyype = typeModel[T] + val inputs = (1 to size).map(i => Flow(Symbol(s"i$i"), tyype, In)).toList + val output = Flow(Symbol(s"min$size${nameOf[T].name}"), tyype, Out) + OperatorModel(PhylogFolder.genericOperatorFolder, inputs, output, + s"""assert + |$output = case { + |${ + allOf[T].toList.sorted.map(fm => inputs.map(f => s"$f = ${fm.name.name}").mkString(" or ") + s": ${fm.name.name},").mkString("", "\n", s"\nelse ${noneOf[T].name.name}") + } + |}; + """.stripMargin) + } + + private val _worstMemo = collection.mutable.HashMap.empty[Set[String],(String,Seq[String],SubComponent)] + + def mkWorstSub[T: IsFinite : IsCriticityOrdering](l:Set[String]): (String,Seq[String],SubComponent) = _worstMemo.getOrElseUpdate(l, + { + val model = WorstModelHelper[T](l.size) + val uuid = _worstMemo.keys.count(p => p.size == l.size) + val subName = s"${model.name.name}$uuid" + val worstAssertions = model.inputs.map{f => s"$subName.$f"} zip l map (p => s"${p._1} = ${p._2};") + (s"$subName.${model.result}",worstAssertions,SubComponent(Symbol(subName),model.model)) + }) + + private case class WorstModelHelper[T: IsFinite : IsCriticityOrdering](size: Int) { + private val tyype = typeModel[T] + val inputs: List[Flow] = (1 to size).map(i => Flow(Symbol(s"i$i"), tyype, In)).toList + val result: Flow = Flow(Symbol("o"), tyype,Out) + val name : Symbol = Symbol(s"worst$size${nameOf[T].name}") + val model: ComponentModel = ComponentModel( + name, + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.maxBlue, + GenericImage.maxGreen :: GenericImage.maxRed :: Nil, + inputs :+ result, + Nil, + Nil, + s"""assert + |$result = case { + |${allOf[T].toList.sorted.reverse.map(fm => inputs.map(f => s"${f.name.name} = ${fm.name.name}").mkString(" or ") + s": ${fm.name.name},").mkString("", "\n", s"\nelse ${noneOf[T]}")} + |}; + """.stripMargin) + } + + def maxOperator[T: IsFinite : IsCriticityOrdering](size: Int): OperatorModel = { + val tyype = typeModel[T] + val inputs = (1 to size).map(i => Flow(Symbol(s"i$i"), tyype, In)).toList + val output = Flow(Symbol(s"max$size${nameOf[T].name}"), tyype, Out) + OperatorModel(PhylogFolder.genericOperatorFolder, inputs, output, + s"""assert + |$output = case { + |${allOf[T].toList.sorted.reverse.map(fm => inputs.map(f => s"${f.name.name} = ${fm.name.name}").mkString(" or ") + s": ${fm.name.name},").mkString("", "\n", s"\nelse ${noneOf[T]}")} + |}; + """.stripMargin) + } + + + def mkContainerShadowSub[T: IsFinite : IsShadowOrdering](name: Symbol, newMode:String, containerMode:String): (String,Seq[String],SubComponent) = { + val model = new ContainerShadowHelper[T]() + val worstAssertions = s"${name.name}.${model.newMode} = $newMode;" :: s"${name.name}.${model.containerMode} = $containerMode;" :: Nil + (s"${name.name}.${model.output}",worstAssertions,SubComponent(name,model.model)) + } + + private class ContainerShadowHelper[T: IsFinite : IsShadowOrdering] { + private val tyype = typeModel[T] + val newMode: Flow = Flow(Symbol("new"), tyype, In) + val containerMode: Flow = Flow(Symbol("container"), tyype, In) + val output: Flow = Flow(Symbol("o"), tyype, Out) + private val impacted = allOf[T].map(container => container -> allOf[T].filter(current => current.containerShadow(container) == container)).filter(_._2.nonEmpty) + val model: ComponentModel = ComponentModel( + Symbol(s"containerShadow${nameOf[T].name}"), + SubFamilyFolder(tyype.name, PhylogFolder.fmOperatorsFamilyFolder), + GenericImage.maxBlue, + GenericImage.maxGreen :: GenericImage.maxRed :: Nil, + newMode :: containerMode :: output :: Nil, + Nil, + Nil, + s"""assert + |$output = case { + |${impacted.map(p => "(" + p._2.map(current => s"$newMode = $current").mkString("(", " or ", ")") + s" and $containerMode = ${p._1.name.name})").mkString(" or ")} : $containerMode, + |else $newMode + |}; + """.stripMargin) + } + + def containerShadowOperator[T: IsFinite : IsShadowOrdering]: OperatorModel = { + val tyype = typeModel[T] + val newMode = Flow(Symbol("new"), tyype, In) + val containerMode = Flow(Symbol("container"), tyype, In) + val output = Flow(Symbol(s"containerShadow${nameOf[T].name}"), tyype, Out) + val impacted = allOf[T].map(container => container -> allOf[T].filter(current => current.containerShadow(container) == container)).filter(_._2.nonEmpty) + OperatorModel( + PhylogFolder.genericOperatorFolder, newMode :: containerMode :: Nil, output, + s"""assert + |$output = case { + |${impacted.map(p => "(" + p._2.map(current => s"$newMode = $current").mkString("(", " or ", ")") + s" and $containerMode = ${p._1.name.name})").mkString(" or ")} : $containerMode, + |else $newMode + |}; + """.stripMargin + ) + } + + def inputShadowOperator[T: IsFinite : IsShadowOrdering]: OperatorModel = { + val tyype = typeModel[T] + val newMode = Flow(Symbol("new"), tyype, In) + val containerMode = Flow(Symbol("container"), tyype, In) + val output = Flow(Symbol(s"inputShadow${nameOf[T].name}"), tyype, Out) + val impacted = allOf[T].map(container => container -> allOf[T].filter(current => current.inputShadow(container) == container)).filter(_._2.nonEmpty) + OperatorModel( + PhylogFolder.genericOperatorFolder, newMode :: containerMode :: Nil, output, + s"""assert + |$output = case { + |${impacted.map(p => "(" + p._2.map(current => s"$newMode = $current").mkString("(", " or ", ")") + s" and $containerMode = ${p._1.name.name})").mkString(" or ")} : $containerMode, + |else $newMode + |}; + """.stripMargin + ) + } + + case class WorstSchedulerTopHelper(size:Int) { + val sonDirection: List[Flow] = (1 to size).map(i => Flow(Symbol(s"sonDirection$i"), typeModel[Direction.Value], In)).toList + val fireOrders: List[Flow] = (1 to size).map(i => Flow(Symbol(s"sonFire$i"), typeModel[Fire.Value], Out)).toList + private val fireOrdersAssertions = fireOrders.zip(sonDirection).map( p => + s"""${p._1} = case { + |${p._2} = ${Direction.Degradation} : ${Fire.Apply}, + |${sonDirection.filterNot(_ == p._2).map(other => s"$other = ${Direction.Degradation}").mkString(" or ")} : ${Fire.Wait}, + |else ${Fire.No} + |};""".stripMargin + ) + val model: ComponentModel = ComponentModel( + Symbol(s"worstScheduler$size"), + SubFamilyFolder(typeModel[Fire.Value].name, PhylogFolder.phylogComponentFolder), + GenericImage.maxBlue, + GenericImage.maxGreen :: GenericImage.maxRed :: Nil, + sonDirection ++ fireOrders , + Nil, + Nil, + s"""assert + |${fireOrdersAssertions.mkString("\n")} + |""".stripMargin) + } + + def authorizeOperator[FM: IsFinite]: OperatorModel = { + val tyype = typeModel[FM] + val initial = Flow(Symbol(s"initial"), tyype, In) + val reject = Flow(Symbol("reject"), CeciliaBoolean, In) + val output = Flow(Symbol(s"authorize${nameOf[FM].name}"), tyype, Out) + OperatorModel( + PhylogFolder.genericOperatorFolder, initial :: reject :: Nil, output, + s"""assert + |${s"$output = (if $reject then ${noneOf[FM].name.name} else $initial);"} + """.stripMargin + ) + } +} diff --git a/src/main/scala/views/dependability/exporters/CeciliaExporter.scala b/src/main/scala/views/dependability/exporters/CeciliaExporter.scala new file mode 100644 index 0000000..18f1f15 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/CeciliaExporter.scala @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import pml.exporters.FileManager + +import scala.xml.{Elem, XML} + +trait CeciliaExporter[T] { + type R + + def toCecilia(x: T): R +} + +trait CeciliaExporterOps { + implicit class ceciliaOps[T, O](x: T) { + def toCecilia(implicit ev: CeciliaExporter.Aux[T, O]): O = ev.toCecilia(x) + val ceciliaExportName : String = CeciliaExporter.ceciliaExportName(x) + def exportAsCecilia(implicit ev: CeciliaExporter.Aux[T, O]) : Unit = { + val file = FileManager.exportDirectory.getFile(ceciliaExportName+".xml") + ev.toCecilia(x) + val elem: Elem = { + + {PhylogFolder.allFamilies.map(_.toElem)} + + } + XML.save(file.getAbsolutePath, elem) + } + } +} + +object CeciliaExporter { + + type Aux[T, O] = CeciliaExporter[T] { + type R = O + } + + def ceciliaExportName[T](x:T): String = x match { + case d: DependabilitySpecification => s"$x${d.depSpecificationName.name}" + case _ => x.toString + } +} diff --git a/src/main/scala/views/dependability/exporters/ExprCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/ExprCeciliaExporter.scala new file mode 100644 index 0000000..4b36b10 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/ExprCeciliaExporter.scala @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.model._ +import views.dependability.operators.allOf + +trait ExprCeciliaExporter { + self:BasicOperationCeciliaExporter with TypeCeciliaExporter => + + case class AssertionHelper(result:Map[TargetId,String], subComponentAssertions: Map[SubComponent, Seq[String]]) + + implicit def ExprCeciliaExporter[T] : Aux[Expr[T], AssertionHelper] = new CeciliaExporter[Expr[T]]{ + type R = AssertionHelper + + def boolExprToHelper(x: BoolExpr): AssertionHelper = x match { + case Equal(l,r) => + val lMap = toCecilia(l.asInstanceOf[Expr[T]]) + val rMap = toCecilia(r.asInstanceOf[Expr[T]]) + val keys = lMap.result.keySet.intersect(rMap.result.keySet) + AssertionHelper( + keys.map(k => k -> s"${lMap.result(k)} = ${rMap.result(k)}").toMap, + lMap.subComponentAssertions ++ rMap.subComponentAssertions) + case And(l@_*) => + val lR = l.map(boolExprToHelper) + val keys = lR.foldLeft(allOf[TargetId].toSet)((acc,m) => acc.intersect(m.result.keySet)) + AssertionHelper( + keys.map(k => k -> lR.map(_.result(k)).mkString(" and ")).toMap, + lR.map(_.subComponentAssertions).reduce(_ ++ _) + ) + + case Or(l@_*) => + val lR = l.map(boolExprToHelper) + val keys = lR.foldLeft(allOf[TargetId].toSet)((acc,m) => acc.intersect(m.result.keySet)) + AssertionHelper( + keys.map(k => k -> lR.map(_.result(k)).mkString(" or ")).toMap, + lR.map(_.subComponentAssertions).reduce(_ ++ _) + ) + case Not(n) => + val r = boolExprToHelper(n) + AssertionHelper(r.result.transform((_,v) => s"not $v"),r.subComponentAssertions) + } + + def toCecilia(x: Expr[T]): AssertionHelper = x match { + case ITE(i,t,e) => + val tMap = toCecilia(t) + val eMap = toCecilia(e) + val iMap = boolExprToHelper(i) + val keys = tMap.result.keySet.intersect(eMap.result.keySet).intersect(iMap.result.keySet) + val iteMap = keys.map(k => k -> { + s"""( if (${iMap.result(k)}) then + | ${tMap.result(k)} + | else + | ${eMap.result(k)} + |)""".stripMargin + }).toMap + AssertionHelper(iteMap, tMap.subComponentAssertions ++ eMap.subComponentAssertions ++ iMap.subComponentAssertions) + case Const(x) => + AssertionHelper(allOf[TargetId].map(t => t -> x.toString).toMap,Map.empty) + case DMap(p:Map[TargetId,Expr[_]]) => + val r = p.transform((_,v) => toCecilia(v.asInstanceOf[Expr[T]])) + AssertionHelper(r.transform((k,v) => v.result(k)),r.map(_._2.subComponentAssertions).reduce(_ ++ _)) + case Of(m, id) => + AssertionHelper(allOf[TargetId].map(t => t -> s"$m^$id").toMap, Map.empty) + case Worst(l*) if l.size == 1 => + toCecilia(l.head) + case w@Worst(l*) if l.size > 1 => + val lR = l.map(toCecilia) + val lMap = lR.foldLeft(Map.empty[TargetId,List[String]])((acc,m) => { + m.result.transform((k,v) => (for{a <- acc.get(k)} yield a :+ v) getOrElse List(v)) + }) + val worsts = lMap.transform((k,v) => mkWorstSub(v.toSet)(w.finite,w.ordering)) + AssertionHelper( + worsts.transform((_,v) => v._1), + lR.map(_.subComponentAssertions).reduce(_ ++ _) ++ worsts.map(p => p._2._3 -> p._2._2) + ) + case Best(l*) if l.size == 1 => + toCecilia(l.head) + case b@Best(l*) => + val lR = l.map(toCecilia) + val lMap = lR.foldLeft(Map.empty[TargetId,List[String]])((acc,m) => { + m.result.transform((k,v) => (for{a <- acc.get(k)} yield a :+ v) getOrElse List(v)) + }) + val bests = lMap.transform((k,v) => mkWorstSub(v.toSet)(b.finite,b.ordering)) + AssertionHelper( + bests.transform((_,v) => v._1), + lR.map(_.subComponentAssertions).reduce(_ ++ _) ++ bests.map(p => p._2._3 -> p._2._2) + ) + case v:Variable[_] => + AssertionHelper(allOf[TargetId].map(t => t-> v.toString).toMap,Map.empty) + case b:BoolExpr => boolExprToHelper(b) + } + + } +} diff --git a/src/main/scala/views/dependability/exporters/Folder.scala b/src/main/scala/views/dependability/exporters/Folder.scala new file mode 100644 index 0000000..f2bb6dd --- /dev/null +++ b/src/main/scala/views/dependability/exporters/Folder.scala @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.Folder.date +import views.dependability.exporters.Model._ + +import java.lang.{System => OS} +import scala.xml.Elem + +sealed trait Folder { + val id: Int = Folder.folderNb + val name: Symbol + lazy val absolutePath: String + Folder.folderNb = Folder.folderNb + 1 + + override def toString: String = s"Folder(name=${name.name},id=$id)" +} + +object Folder { + var folderNb = 100 + val date: Long = OS.currentTimeMillis() +} + +object RootFolder extends Folder { + lazy val absolutePath: String = "" + override val name: Symbol = Symbol("") +} + +sealed trait CeciliaFolder extends Folder { + val parent: Folder + lazy val absolutePath: String = parent match { + case RootFolder => name.name + case _ => s"${parent.absolutePath}/${name.name}" + } + + def toElem: Elem +} + +sealed trait SubFolder[T] extends CeciliaFolder + +class FamilyFolder[T] (val name: Symbol)(implicit m: ModelDescriptor[T]) extends SubFolder[T] { + val parent: RootFolder.type = RootFolder + private val elements = scala.collection.mutable.Set.empty[SubFolder[T]] + def add(a:SubFolder[T]):Unit = elements += a + + def toElem: Elem = m.getFamilyFamilyFlag match { + case Some(value) => + + {elements.toList.sortBy(_.id).map(_.toElem)} + + case None => + + {elements.toList.sortBy(_.id).map(_.toElem)} + + } +} + +object FamilyFolder { + def apply[T](name: Symbol)(implicit m: ModelDescriptor[T]): FamilyFolder[T] = m.getFolder(name) +} + +class SubFamilyFolder[T] (val name: Symbol, val parent: FamilyFolder[T])(implicit m: ModelDescriptor[T]) extends SubFolder[T] { + private val elements = scala.collection.mutable.Set.empty[EntityFolder[T]] + parent.add(this) + def add(a:EntityFolder[T]):Unit = elements += a + def toElem: Elem = { + + {elements.toList.sortBy(_.id).map(_.toElem)} + + } +} + +object SubFamilyFolder { + def apply[T](name: Symbol, parent:FamilyFolder[T])(implicit m: ModelDescriptor[T]): SubFamilyFolder[T] = m.getFolder(name,parent) +} + +case class EntityFolder[T](name: Symbol, parent: SubFolder[T])(implicit m: ModelDescriptor[T]) extends SubFolder[T] { + private val elements = scala.collection.mutable.Set.empty[VersionFolder[T]] + def getVersionNb : Int = elements.size + parent match { + case f:FamilyFolder[T] => f.add(this) + case f:SubFamilyFolder[T] => f.add(this) + case _ => + } + def add(a:VersionFolder[T]):Unit = elements += a + def toElem: Elem = m.getEntityFamilyFlag match { + case Some(tag) => + + {elements.toList.sortBy(_.id).map(_.toElem)} + + case None => + + {elements.toList.sortBy(_.id).map(_.toElem)} + + } +} + +case class VersionFolder[T](parent: EntityFolder[T])(implicit m: ModelDescriptor[T]) extends CeciliaFolder { + val name: Symbol = Symbol((parent.getVersionNb + 1).toString) + override lazy val absolutePath: String = s"${parent.absolutePath};${name.name}" + private var model: Option[T] = None + def add(a:T):Unit = model = Some(a) + parent.add(this) + lazy val familyTag:String = { + (for(m <- model) yield m match { + case x:RecordType => "record" + case x:EnumeratedType => "enum" + case _ => "" + }) getOrElse("") + } + def toElem: Elem = m.getVersionFamilyFlag match { + case None => + + {(for (mdl <- model) yield m.toElem(mdl))getOrElse ""} + + + case Some(tag) => + + {(for (mdl <- model) yield m.toElem(mdl))getOrElse ""} + + + + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/exporters/GenericImage.scala b/src/main/scala/views/dependability/exporters/GenericImage.scala new file mode 100644 index 0000000..093ba40 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/GenericImage.scala @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.PhylogFolder._ + +object GenericImage { + + private val load = (x:String) => getClass.getClassLoader.getResourceAsStream(x) + val functionBlueCircle: ImageModel = ImageModel(genericImageFolder, Symbol("function_blue_circle"), load("icons/generic/function_blue_circle.gif")) + val functionGreenCircle: ImageModel = ImageModel(genericImageFolder, Symbol("function_green_circle"), load("icons/generic/function_green_circle.gif")) + val functionRedCircle: ImageModel = ImageModel(genericImageFolder, Symbol("function_red_circle"), load("icons/generic/function_red_circle.gif")) + val functionRedCross: ImageModel = ImageModel(genericImageFolder, Symbol("function_red_cross"), load("icons/generic/function_red_cross.gif")) + val functionPurpleCircle: ImageModel = ImageModel(genericImageFolder, Symbol("function_purple_circle"), load("icons/generic/function_purple_circle.gif")) + val sourceBlueCircle: ImageModel = ImageModel(genericImageFolder, Symbol("source_blue_circle"), load("icons/generic/source_blue_circle.gif")) + val sourceGreenCircle: ImageModel = ImageModel(genericImageFolder, Symbol("source_green_circle"), load("icons/generic/source_green_circle.gif")) + val sourceRedCross: ImageModel = ImageModel(genericImageFolder, Symbol("source_red_cross"), load("icons/generic/source_red_cross.gif")) + val minBlue: ImageModel = ImageModel(genericImageFolder, Symbol("andBlue"), load("icons/generic/andBlue.gif")) + val minRed: ImageModel = ImageModel(genericImageFolder, Symbol("andRed"), load("icons/generic/andRed.gif")) + val minGreen: ImageModel = ImageModel(genericImageFolder, Symbol("andGreen"), load("icons/generic/andGreen.gif")) + val maxBlue: ImageModel = ImageModel(genericImageFolder, Symbol("orBlue"), load("icons/generic/orBlue.gif")) + val maxRed: ImageModel = ImageModel(genericImageFolder, Symbol("orRed"), load("icons/generic/orRed.gif")) + val maxGreen: ImageModel = ImageModel(genericImageFolder, Symbol("orGreen"), load("icons/generic/orGreen.gif")) + val equalBlue: ImageModel = ImageModel(genericImageFolder, Symbol("equalBlue"), load("icons/generic/equalBlue.gif")) + val equalRed: ImageModel = ImageModel(genericImageFolder, Symbol("equalRed"), load("icons/generic/equalRed.gif")) + val equalGreen: ImageModel = ImageModel(genericImageFolder, Symbol("equalGreen"), load("icons/generic/equalGreen.gif")) + val selectBlue: ImageModel = ImageModel(genericImageFolder, Symbol("selectBlue"), load("icons/generic/selectBlue.gif")) + val select1Red: ImageModel = ImageModel(genericImageFolder, Symbol("select1Red"), load("icons/generic/select1Red.gif")) + val select1Green: ImageModel = ImageModel(genericImageFolder, Symbol("select1Green"), load("icons/generic/select1Green.gif")) + val select2Red: ImageModel = ImageModel(genericImageFolder, Symbol("select1Red"), load("icons/generic/select1Red.gif")) + val select2Green: ImageModel = ImageModel(genericImageFolder, Symbol("select1Green"), load("icons/generic/select1Green.gif")) + val preBlue: ImageModel = ImageModel(genericImageFolder, Symbol("preBlue"), load("icons/generic/preBlue.gif")) + val preRed: ImageModel = ImageModel(genericImageFolder, Symbol("preRed"), load("icons/generic/preRed.gif")) + val preGreen: ImageModel = ImageModel(genericImageFolder, Symbol("preGreen"), load("icons/generic/preGreen.gif")) + val phylogBlockBlue: ImageModel = ImageModel(genericImageFolder, Symbol("phylogBlockBlue"), load("icons/phylog/phylogBlockBlue.gif")) + val phylogBlockRed: ImageModel = ImageModel(genericImageFolder, Symbol("phylogBlockRed"), load("icons/phylog/phylogBlockRed.gif")) + val phylogBlockGreen: ImageModel = ImageModel(genericImageFolder, Symbol("phylogBlockGreen"), load("icons/phylog/phylogBlockGreen.gif")) + val phylogBlockGrey: ImageModel = ImageModel(genericImageFolder, Symbol("phylogBlockGrey"), load("icons/phylog/phylogBlockGrey.gif")) + +} diff --git a/src/main/scala/views/dependability/exporters/Model.scala b/src/main/scala/views/dependability/exporters/Model.scala new file mode 100644 index 0000000..1513bdc --- /dev/null +++ b/src/main/scala/views/dependability/exporters/Model.scala @@ -0,0 +1,558 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.Model.{ComponentDescriptor, EnumeratedTypeDescriptor, EquipmentDescriptor, ImgDescriptor, ModelDescriptor} + +import java.awt.image.BufferedImage +import java.io.{ByteArrayInputStream, File, InputStream} +import java.nio.file.{Path, Paths} +import javax.imageio.ImageIO +import scala.xml.Elem + +sealed trait CeciliaType + +object CeciliaBoolean extends CeciliaType { + val name: Symbol = Symbol("bool") +} + +sealed trait Orientation { + val name: String +} + +case object In extends Orientation { + val name: String = "in" +} + +case object Out extends Orientation { + val name: String = "out" +} + +case object Local extends Orientation{ + val name: String = "local" +} + +case class Configuration(name: Symbol, conf: Map[State, String]) { + def toElem: Elem = { + + {conf.map(p => )} + + } +} + +case class State(name: Symbol, tyype: CeciliaType, ini: String) { + def toElem: Elem = tyype match { + case CeciliaBoolean => + case tyype: EnumeratedType => + case tyype: RecordType => + } + + override def toString: String = name.name +} + +case class Flow(name: Symbol, tyype: CeciliaType, orientation: Orientation) { + private var _owner: Option[BlockModel[_]] = None + + override def toString: String = name.name + + def add(to: BlockModel[_]): Unit = _owner = Some(to) + + def owner: BlockModel[_] = _owner.get + + def toElem(x: Int, y: Int, id: Int): Elem = tyype match { + case CeciliaBoolean => + + case tyype: EnumeratedType => + + case tyype: RecordType => + + } + + def toElem: Elem = tyype match { + case CeciliaBoolean => + + case tyype: EnumeratedType => + + case tyype: RecordType => + + } +} + +sealed trait Model { + def toElem: Elem +} + +object Model { + + trait ModelDescriptor[T] { + private val families = collection.mutable.HashMap.empty[Path, FamilyFolder[T]] + private val subFamilies = collection.mutable.HashMap.empty[Path, SubFamilyFolder[T]] + private val models = collection.mutable.HashMap.empty[Path, T] + + def getName: String + + def getVersionFamilyFlag: Option[String] + + def getEntityFamilyFlag: Option[String] + + def getFamilyFamilyFlag: Option[String] + + def toElem(a: T): Elem + + def getFolder(s: Symbol): FamilyFolder[T] = + families.getOrElseUpdate(Paths.get(s.name), new FamilyFolder[T](s)(this)) + + def getFolder(s: Symbol, parent: FamilyFolder[T]): SubFamilyFolder[T] = + subFamilies.getOrElseUpdate(Paths.get(parent.absolutePath, s.name), new SubFamilyFolder(s, parent)(this)) + + def getModel(of: Path)(d: () => T): T = models.getOrElseUpdate(of, d()) + } + + object ModelDescriptor + + implicit object ImgDescriptor extends ModelDescriptor[ImageModel] { + def getName: String = "imag" + + def getVersionFamilyFlag: Option[String] = None + + override def getEntityFamilyFlag: Option[String] = None + + override def getFamilyFamilyFlag: Option[String] = None + + def toElem(a: ImageModel): Elem = a.toElem + } + + implicit object ComponentDescriptor extends ModelDescriptor[ComponentModel] { + def getName: String = "component" + + def toElem(a: ComponentModel): Elem = a.toElem + + def getVersionFamilyFlag: Option[String] = None + + override def getEntityFamilyFlag: Option[String] = None + + override def getFamilyFamilyFlag: Option[String] = None + } + + implicit object EnumeratedTypeDescriptor extends ModelDescriptor[EnumeratedType] { + def getName: String = "type" + + def getVersionFamilyFlag: Option[String] = Some("enum") + + def toElem(a: EnumeratedType): Elem = a.toElem + + override def getEntityFamilyFlag: Option[String] = Some("enum") + + override def getFamilyFamilyFlag: Option[String] = None + } + + implicit object RecordTypeDescriptor extends ModelDescriptor[RecordType] { + def getName: String = "type" + + def toElem(a: RecordType): Elem = a.toElem + + def getVersionFamilyFlag: Option[String] = Some("record") + + override def getEntityFamilyFlag: Option[String] = Some("record") + + override def getFamilyFamilyFlag: Option[String] = None + } + + implicit object SystemDescriptor extends ModelDescriptor[SystemModel] { + def getName: String = "project" + + def getVersionFamilyFlag: Option[String] = Some("model") + + def toElem(a: SystemModel): Elem = a.toElem + + override def getEntityFamilyFlag: Option[String] = Some("model") + + override def getFamilyFamilyFlag: Option[String] = Some("project") + } + + implicit object EquipmentDescriptor extends ModelDescriptor[EquipmentModel] { + def getName: String = "equipment" + + def toElem(a: EquipmentModel): Elem = a.toElem + + def getVersionFamilyFlag: Option[String] = None + + override def getEntityFamilyFlag: Option[String] = None + + override def getFamilyFamilyFlag: Option[String] = None + } + + implicit object OperatorDescriptor extends ModelDescriptor[OperatorModel] { + def getName: String = "operator" + + def toElem(a: OperatorModel): Elem = a.toElem + + def getVersionFamilyFlag: Option[String] = None + + override def getEntityFamilyFlag: Option[String] = None + + override def getFamilyFamilyFlag: Option[String] = None + } + + trait FlowPlacer { + def position(f: Flow): (Int, Int) + + def idOf(f: Flow): Int + } + + trait EquiBlockFlowPlacer extends FlowPlacer { + self: BlockModel[_] => + private val _basicSep = 10 + private val _inputs = flows.filter(_.orientation == In) + private val _outputs = flows.filter(_.orientation == Out) + private val _inputMargin = (icon.height - _basicSep * (_inputs.size - 1)) / 2 + private val _outputMargin = (icon.height - _basicSep * (_outputs.size - 1)) / 2 + + def position(f: Flow): (Int, Int) = f match { + case Flow(n, _, In) => + if (_inputMargin > 0) + (0, _inputMargin + _inputs.indexWhere(_.name == n) * _basicSep) + else + (0, _inputs.indexWhere(_.name == n) * icon.height / _inputs.size) + case Flow(n, _, Out) => + if (_outputMargin > 0) + (icon.width, _outputMargin + _outputs.indexWhere(_.name == n) * _basicSep) + else + (0, _outputs.indexWhere(_.name == n) * icon.height / _outputs.size) + case _ => (0,0) + } + + def idOf(f: Flow): Int = { + flows.indexWhere(_.name == f.name) + } + } + +} + + +class EnumeratedType(val name: Symbol, val values: List[Symbol], val parent: VersionFolder[EnumeratedType]) extends CeciliaType { + parent.add(this) + + def toElem: Elem = { + + + {values.map(v => )} + + + } +} + +object EnumeratedType { + def apply(name: Symbol, + values: List[Symbol], + parent: SubFamilyFolder[EnumeratedType])(implicit ev: ModelDescriptor[EnumeratedType]): EnumeratedType = + ev.getModel(Paths.get(parent.absolutePath, name.name))(() => new EnumeratedType(name, values, VersionFolder(EntityFolder(name, parent)))) +} + +class RecordType(val name: Symbol, val parent: VersionFolder[RecordType], val fields: List[Flow]) extends CeciliaType { + parent.add(this) + + def toElem: Elem = { + + + {fields.collect({ + case Flow(n, CeciliaBoolean, _) => + + case Flow(n, tyype: EnumeratedType, _) => + + })} + + + } +} + +object RecordType { + def apply(name: Symbol, + parent: SubFamilyFolder[RecordType], + fields: List[Flow])(implicit ev: ModelDescriptor[RecordType]): RecordType = + ev.getModel(Paths.get(parent.absolutePath, name.name))(() => new RecordType(name, VersionFolder(EntityFolder(name, parent)), fields.distinct)) +} + +sealed trait BlockModel[T] extends Model.EquiBlockFlowPlacer { + val name: Symbol + val parent: VersionFolder[T] + val icon: ImageModel + val simul: List[ImageModel] + val flows: List[Flow] + val code: String +} + +sealed trait EventModel { + val name: Symbol +} + +case class SynchroEventModel(name: Symbol, events: List[EventModel], tyype: String) extends EventModel + +sealed trait ConcreteEventModel extends EventModel { + val law: Elem + + def toElem: Elem = { + + {law} + + } +} + +case class DeterministicEventModel(name: Symbol) extends ConcreteEventModel { + val law: Elem = { + + + + } +} + +case class StochastiqueEventModel(name: Symbol, lambda: Double = 10e-4) extends ConcreteEventModel { + val law: Elem = { + + + + } +} + +case class ComponentModel( + name: Symbol, + parent: VersionFolder[ComponentModel], + icon: ImageModel, + simul: List[ImageModel], + flows: List[Flow], + events: List[ConcreteEventModel], + states: List[State], + code: String + ) extends BlockModel[ComponentModel] { + parent.add(this) + flows.foreach(_.add(this)) + + def toElem: Elem = { + + + {simul.map(s => )}{flows.map(flow => flow.toElem(position(flow)._1, position(flow)._2, idOf(flow)))}{states.map(_.toElem)}{events.map(_.toElem)} + {code} + + + } +} + +object ComponentModel { + val _components = collection.mutable.HashMap.empty[(Symbol,SubFamilyFolder[ComponentModel]), ComponentModel] + def apply(name: Symbol, + parent: SubFamilyFolder[ComponentModel], + icon: ImageModel, + simul: List[ImageModel], + flows: List[Flow], + events: List[ConcreteEventModel], + states: List[State], + code: String): ComponentModel = { + _components.getOrElseUpdate((name,parent), + ComponentModel( + name, + VersionFolder(EntityFolder(name, parent)), + icon, + simul, + flows.distinct, + events.distinct, + states.distinct, + code)) + } +} + + +case class SubComponent(name: Symbol, tyype: BlockModel[_], x: Int = 0, y: Int = 0, mirrorV: Boolean = false, mirrorH: Boolean = false) { + def toElem: Elem = { + + } + + override def toString: String = name.name +} + +class EquipmentModel( + val name: Symbol, + val parent: VersionFolder[EquipmentModel], + val icon: ImageModel, + val simul: List[ImageModel], + val flows: List[Flow], + val subs: List[SubComponent], + val links: List[(Flow, Flow)], + val sync: List[SynchroEventModel] = Nil, + val code: String) extends BlockModel[EquipmentModel] { + parent.add(this) + flows.foreach(_.add(this)) + + def toElem: Elem = { + + + {simul.map(s => )}{flows.map(flow => flow.toElem(position(flow)._1, position(flow)._2, idOf(flow)))}{subs.map(_.toElem)}{links.zipWithIndex.map(l => )}{sync.map(s => { + + {s.events.map(e => )} + + })} + + {links.indices.map(i => )}{flows.map(f => )}{subs.map(s => )} + + + {code} + + + + } +} + +object EquipmentModel { + def apply(name: Symbol, + parent: SubFamilyFolder[EquipmentModel], + icon: ImageModel, + simul: List[ImageModel], + flows: List[Flow], + subs: List[SubComponent], + links: List[(Flow, Flow)], + sync: List[SynchroEventModel], + code: String)(implicit ev: ModelDescriptor[EquipmentModel]): EquipmentModel = { + val version = VersionFolder(EntityFolder(name, parent)) + ev.getModel(Paths.get(version.absolutePath))(() => new EquipmentModel(name, version, icon, simul, flows.distinct, subs.distinct, links, sync, code)) + } +} + +class SystemModel(val name: Symbol, + val parent: VersionFolder[SystemModel], + val subs: List[SubComponent], + val links: List[(Flow, Flow)], + val sync: List[SynchroEventModel] = Nil, + val confs: List[Configuration] , + val code: String) extends Model { + parent.add(this) + + def toElem: Elem = { + + + {subs.map(s => )}{links.zipWithIndex.map(l => )}{sync.map(s => { + + {s.events.map(e => )} + + })} + + {links.indices.map(i => )}{subs.map(s => )} + + + + {confs.map(_.toElem)}{code} + + + + } +} + +object SystemModel { + def apply( + name: Symbol, + parent: SubFamilyFolder[SystemModel], + subs: List[SubComponent], + links: List[(Flow, Flow)], + sync: List[SynchroEventModel], + conf: List[Configuration], + code: String)(implicit ev: ModelDescriptor[SystemModel]): SystemModel = { + val version = VersionFolder(EntityFolder(name, parent)) + ev.getModel(Paths.get(version.absolutePath))(() => new SystemModel(name, version, subs.distinct, links, sync, conf, code)) + } +} + +class ImageModel(val parent: VersionFolder[ImageModel], val stream: InputStream) extends Model { + private val data = Array.ofDim[Byte](stream.available()) + //WARNING CAN FAIL TO LOAD ALL BYTE BEFORE READ + stream.read(data) + private val encoded = java.util.Base64.getEncoder.encodeToString(data) + private val img: BufferedImage = ImageIO.read(new ByteArrayInputStream(data)) + + val width: Int = img.getWidth() + val height: Int = img.getHeight + + parent.add(this) + + def toElem: Elem = { + + {encoded} + + } +} + +object ImageModel { + def apply(parent: FamilyFolder[ImageModel], name: Symbol, stream: InputStream)(implicit m: ModelDescriptor[ImageModel]): ImageModel = { + val version = VersionFolder(EntityFolder(name, parent)) + m.getModel(Paths.get(version.absolutePath))(() => new ImageModel(version, stream)) + } + +} + +class OperatorModel(val parent: VersionFolder[OperatorModel], val inputs: List[Flow], val output: Flow, val code: String) extends Model { + parent.add(this) + + def idOf(f: Flow): Int = inputs.indexOf(f) + + def toElem: Elem = { + + {output match { + case Flow(_, CeciliaBoolean, _) => + + {inputs.map(_.toElem)} + {code} + + + + case Flow(_, tyype: EnumeratedType, _) => + + {inputs.map(_.toElem)} + {code} + + + + case Flow(_, tyype: RecordType, _) => + + {inputs.map(_.toElem)} + {code} + + + }} + + } +} + +object OperatorModel { + def apply(parent: FamilyFolder[OperatorModel], inputs: List[Flow], output: Flow, code: String)(implicit ev: ModelDescriptor[OperatorModel]): OperatorModel = { + val version = output.tyype match { + case CeciliaBoolean => + VersionFolder(EntityFolder(output.name, SubFamilyFolder(CeciliaBoolean.name, parent))) + case tyype: EnumeratedType => + VersionFolder(EntityFolder(output.name, SubFamilyFolder(tyype.name, parent))) + case tyype: RecordType => + VersionFolder(EntityFolder(output.name, SubFamilyFolder(tyype.name, parent))) + } + ev.getModel(Paths.get(version.absolutePath))(() => new OperatorModel(version, inputs, output, code)) + } +} + +case class FailureConditions(fc : Set[(String,String)], size:Int) extends Model { + def fileName(f:String,v: String): String = s"${f.replace(".","_")}_is_$v.seq" + def toElem: Elem = { + + {fc.map(p => )} + + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/exporters/PhylogFolder.scala b/src/main/scala/views/dependability/exporters/PhylogFolder.scala new file mode 100644 index 0000000..9a6ccb9 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/PhylogFolder.scala @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +object PhylogFolder { + val genericImageFolder: FamilyFolder[ImageModel] = FamilyFolder(Symbol("generic")) + val phylogComponentFolder: FamilyFolder[ComponentModel] = FamilyFolder(Symbol("phylog")) + val genericOperatorFolder: FamilyFolder[OperatorModel] = FamilyFolder(Symbol("generic")) + val phylogEquipmentFolder: FamilyFolder[EquipmentModel] = FamilyFolder(Symbol("phylog")) + val phylogRecordTypeFolder: FamilyFolder[RecordType] = FamilyFolder(Symbol("phylogRecord")) + val phylogEnumTypeFolder: FamilyFolder[EnumeratedType] = FamilyFolder(Symbol("phylogEnum")) + val fmOperatorsFamilyFolder: FamilyFolder[ComponentModel] = FamilyFolder(Symbol("operators")) + val phylogSystemFamilyFolder: FamilyFolder[SystemModel] = FamilyFolder[SystemModel](Symbol("phylog")) + + val phylogInitiatorFolder: SubFamilyFolder[EquipmentModel] = SubFamilyFolder(Symbol("initiator"), phylogEquipmentFolder) + val phylogTargetFolder: SubFamilyFolder[EquipmentModel] = SubFamilyFolder(Symbol("target"), phylogEquipmentFolder) + val phylogTransporterFolder: SubFamilyFolder[EquipmentModel] = SubFamilyFolder(Symbol("transporter"), phylogEquipmentFolder) + val automatonFamilyFolder: SubFamilyFolder[ComponentModel] = SubFamilyFolder(Symbol("block"), phylogComponentFolder) + val phylogFrameworkComponentFolder: SubFamilyFolder[ComponentModel] = SubFamilyFolder(Symbol("framework"), phylogComponentFolder) + val phylogFMTypeFolder: SubFamilyFolder[EnumeratedType] = SubFamilyFolder(Symbol("failureModeTypes"), phylogEnumTypeFolder) + val phylogCustomTypeFolder: SubFamilyFolder[RecordType] = SubFamilyFolder(Symbol("customTypes"), phylogRecordTypeFolder) + val phylogSystemExampleFolder: SubFamilyFolder[SystemModel] = SubFamilyFolder(Symbol("example"), phylogSystemFamilyFolder) + + val allFamilies: List[FamilyFolder[_]] = + (genericImageFolder :: phylogComponentFolder :: genericOperatorFolder :: + phylogEquipmentFolder :: fmOperatorsFamilyFolder :: phylogEnumTypeFolder :: phylogRecordTypeFolder :: phylogSystemFamilyFolder :: Nil).sortBy(_.id) +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/exporters/PlatformCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/PlatformCeciliaExporter.scala new file mode 100644 index 0000000..2573fb6 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/PlatformCeciliaExporter.scala @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import pml.exporters.FileManager +import pml.model.hardware.{Hardware, Platform, Virtualizer, Initiator as PMLInitiator, SimpleTransporter as PMLSimpleTransporter, Target as PMLTarget, Transporter as PMLTransporter} +import pml.model.service.Service +import pml.model.software.Application as PMLApplication +import pml.operators.* +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.model.CustomTypes.TargetStatus +import views.dependability.model.{Expr, InitiatorId, InputDepTarget, InputInDepTarget, Software, SoftwareId, TargetId, TransporterId, Variable, Application as DepApplication, BasicTransporter as DepBasicTransporter, Initiator as DepInitiator, SimpleTransporter as DepSimpleTransporter, System as DepSystem, Virtualizer as DepVirtualizer} +import views.dependability.operators.{IsCriticityOrdering, IsFinite, IsShadowOrdering} + +import scala.reflect._ +import scala.xml.XML + +trait PlatformCeciliaExporter { + self: BasicOperationCeciliaExporter with SystemCeciliaExporter with TypeCeciliaExporter => + + implicit class platformExportOps[ + FM : IsCriticityOrdering : IsFinite : IsShadowOrdering, + T <: Platform with DependabilitySpecification.Aux[FM] : Typeable](a: T) { + def exportAsCeciliaWithFM(): Unit = { + a.exportAsCecilia(platformIsExportable[FM,T]) + } + } + + trait DependabilitySpecification { + self : Platform => + + type U + + implicit val toTargetId : PMLTarget => TargetId = mkTargetId + + val depSpecificationName: Symbol + + val failureConditions: Set[(PMLApplication,U,Int)] + + def mkTargetId(t:PMLTarget):TargetId + + def softwareStoresDependency(p:PMLApplication) : (Variable[U], Variable[TargetStatus[U]]) => Expr[TargetStatus[U]] + + def softwareState(p:PMLApplication) : (Variable[U], Variable[TargetStatus[U]]) => Expr[U] + + val targetIsInputDep: Set[PMLTarget] + } + + object DependabilitySpecification{ + type Aux[T] = DependabilitySpecification { + type U = T + } + } + + private def platformIsExportable[ + FM : IsCriticityOrdering : IsFinite : IsShadowOrdering, + T <: Platform with DependabilitySpecification.Aux[FM]: Typeable]: Aux[T, SystemModel] = new CeciliaExporter[T] { + type R = SystemModel + + def toCecilia(x: T): SystemModel = { + import x._ + object TempSystem extends DepSystem(x.name) { + + def getTargetId(b: Service): Set[TargetId] = { + b.hardwareOwner.collect { case t: PMLTarget => mkTargetId(t) } + } + + def isReachable(from: Service, to: Service, in: Map[Service, Set[Service]]): Boolean = + from == to || in.contains(from) && in(from).exists(succ => isReachable(succ, to, in)) + + def getRouted(on: Service): (InitiatorId, TargetId) => Boolean = (i, t) => { + val ini = x.initiators.filter { _.name == i.name } + val tgt = x.targets.filter { _.name == t.name } + ini.exists(pmlI => x.applications.filter(_.hostingInitiators.contains(pmlI)).exists(pmlS => tgt.exists(pmlT => pmlT.services.exists(tb => { + val r = x.serviceGraphOf(pmlS, tb) + (r.keySet ++ r.values.flatten).contains(on) + // InitiatorRouting.get((pmlI, tb, on)) match { + // case Some(next) => + // val r = x.restrictServiceTo(pmlS, tb) + // next.exists(b => isReachable(b, tb, r)) // it exists a next service from which one of the service of t is reachable + // case None => true //not always true, the transaction ini -> on -> tgt must exists + // } + })))) + } + + val getAuthorized: (InitiatorId, TargetId) => Boolean = (i, t) => { + val ini = x.initiators.filter { _.name == i.name } + val s = x.applications.filter(_.hostingInitiators.intersect(ini).nonEmpty) + val tgt = PLProvideService.domain.collect { case t2: PMLTarget if t.name == t2.name => t2 } + s.exists(pmlS => tgt.exists(t => t.services.exists(b => { + SWAuthorizeService(pmlS).contains(b) + }))) + } + + // extract hw connection graph only contains physical connection used by at least one transaction (OR NOT ...) + val hwGraph: Map[Hardware, Set[Hardware]] = x.applications flatMap { + x.hardwareGraphOf + } groupBy (_._1) transform ((_, v) => v.flatMap(_._2)) filter { + _._2.nonEmpty + } + private val hwLinks = hwGraph.keySet.flatMap({ k => hwGraph(k) map { x => (k, x) } }) + private val hwComponents = hwLinks.flatMap({ p => Set(p._1, p._2) }) + + // Transform targets + private val targets = hwComponents.collect { + case t: PMLTarget if targetIsInputDep.contains(t) => t -> InputDepTarget[FM](mkTargetId(t)) + case t:PMLTarget => t -> InputInDepTarget[FM](mkTargetId(t)) + }.toMap + //Transform software to their dependability counterpart where dependencies are load targets and impacts are stores + val software: Map[PMLApplication, Software[FM]] = + x.applications.collect { + case a: PMLApplication => + val stores = softwareStoresDependency(a) + val state = softwareState(a) + a -> DepApplication[FM](SoftwareId(a.name), state, stores) + }.toMap + + //Transform simple transporters, virtualizers and smart to classic transporters and consider authorize and routing + //as rejection + val transporters: Map[PMLTransporter, DepBasicTransporter[FM]] = hwComponents.collect { + case t: PMLSimpleTransporter => + // add routing table to rejection + val reject = (p: (InitiatorId, TargetId)) => t.services.exists(b => !getRouted(b)(p._1, p._2)) + t -> DepSimpleTransporter(TransporterId(t.name), reject) + case v: Virtualizer => + // add routing table and authorize to rejection + val reject = (p: (InitiatorId, TargetId)) => v.services.exists(b => !(getRouted(b)(p._1, p._2) && getAuthorized(p._1, p._2))) + v -> DepVirtualizer(TransporterId(v.name), reject) + }.toMap + + val initiators:Map[PMLInitiator, DepInitiator[FM]]= hwComponents.collect { + case i: PMLInitiator => + i -> DepInitiator(InitiatorId(i.name)) + }.toMap + + // Compute inverse relation + private val use = hwComponents.map(in => in -> hwLinks.collect { + case (b, a) if a == in => b + }).filter(_._2.nonEmpty).toMap + + //connect + hwGraph.foreach { + case (in: PMLInitiator, outs) => + initiators(in).loadI := outs.collect { + case t: PMLTransporter => + transporters(t).loadO + case t: PMLTarget => + targets(t).loadO + }.toList + case (in: PMLTransporter, outs) => + transporters(in).loadI := outs.collect { + case t: PMLTransporter => + transporters(t).loadO + case t: PMLTarget => + targets(t).loadO + }.toList + case _ => println("should not be reachable...") + } + use.foreach { + case (in: PMLTarget, outs) => + targets(in).storeI := outs.collect({ + case i:PMLInitiator => initiators(i).storeO + case t:PMLTransporter => transporters(t).storeO + }).toList + case (in: PMLTransporter, outs) => + transporters(in).storeI := outs.collect { + case t: PMLTransporter => transporters(t).storeO + case t: PMLInitiator => initiators(t).storeO + }.toList + case _ => println("should not be reachable...") + } + x.SWUseInitiator._inverse.foreach { p => { + val t = initiators(p._1) + val sw : List[Software[FM]]= p._2.map(software).toList + t.storeI := sw.map(_.storeO) + sw.foreach{ + case a: DepApplication[FM] => + a.coreState := t.fMAutomaton.o + a.loadI := t.loadO + case d => + d.loadI := t.loadO + } + }} + } + val fSets = failureConditions.groupMap(p => p._3)(p => + (variablePathName(TempSystem,TempSystem.software(p._1).asInstanceOf[DepApplication[FM]].stateO),p._2.toString) + ) + fSets.transform((k,v) => FailureConditions(v,k)).foreach(p => { + val file = FileManager.exportDirectory.getFile(CeciliaExporter.ceciliaExportName(x) + s"FC${p._1}.xml") + XML.save(file.getAbsolutePath, p._2.toElem) + }) + TempSystem.toCecilia + } + } +} diff --git a/src/main/scala/views/dependability/exporters/SoftwareCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/SoftwareCeciliaExporter.scala new file mode 100644 index 0000000..3d46273 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/SoftwareCeciliaExporter.scala @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.exporters.GenericImage._ +import views.dependability.exporters.PhylogFolder.phylogInitiatorFolder +import views.dependability.model.{Application, Descriptor, TargetId} +import views.dependability.operators._ + +trait SoftwareCeciliaExporter { + self: TypeCeciliaExporter with BasicOperationCeciliaExporter with ExprCeciliaExporter=> + + implicit def dependencySpecifiedSoftwareIsExportable[FM: IsFinite : IsCriticityOrdering]: Aux[Application[FM], EquipmentModel] = new CeciliaExporter[Application[FM]] { + type R = EquipmentModel + + def toCecilia(x: Application[FM]): EquipmentModel = { + val tyype = typeModel[FM] + val rTyype = typeModel[TargetId, FM] + val coreState = Flow(x.coreState.id.name, tyype, In) + val requestO = Flow(x.storeO.id.name, rTyype, Out) + val stateO = Flow(x.stateO.id.name, tyype, Out) + val accessI = Flow(x.loadI.id.name, rTyype, In) + val stateExpr = ExprCeciliaExporter.toCecilia(x.softwareState(x.coreState,x.loadI)) + val storeExpr = ExprCeciliaExporter.toCecilia(x.stores(x.coreState,x.loadI)) + val subComponents = + stateExpr.subComponentAssertions.keySet ++ storeExpr.subComponentAssertions.keySet + val subComponentAssertions = + (stateExpr.subComponentAssertions.values ++ storeExpr.subComponentAssertions.values).flatten.toList.sorted + EquipmentModel( + x.id.name, + phylogInitiatorFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + coreState :: accessI :: requestO :: stateO :: Nil, + subComponents.toList, + Nil, + Nil, + s"""assert + |// subcomponent assertions + |${subComponentAssertions.sorted.mkString("\n")} + | + |// provide store status according to dependencies + |${allOf[TargetId].map(k => s"$requestO^$k = ${(for(s <- storeExpr.result.get(k)) yield s) getOrElse noneOf[FM].name.name};").sorted.mkString("\n")} + | + |// state of the software + |$stateO = ${stateExpr.result.values.head}; + | + |// icon management + |icone = (if $stateO = ${min[FM].name.name} then 1 else 2); + |""".stripMargin + ) + } + } + + implicit def descriptorIsExportable[FM: IsFinite : IsCriticityOrdering]: Aux[Descriptor[FM], EquipmentModel] = new CeciliaExporter[Descriptor[FM]] { + type R = EquipmentModel + def toCecilia(x: Descriptor[FM]): EquipmentModel = { + val requestType = typeModel[TargetId, FM] + val loadI = Flow(x.loadI.id.name,requestType,In) + val storeO = Flow(x.storeO.id.name, requestType,Out) + val storeDependency = x.transferts.flatMap(c => c.targetWritten.map(store => store -> c.targetNeeded)).filter(_._2.nonEmpty).toMap + val worst = storeDependency.transform((_,v) => mkWorstSub(v.map(t => s"$loadI^$t"))) + val storeAssertions = allOf[TargetId].map( tId => + worst.get(tId) match { + case None => s"$storeO^$tId = ${noneOf[FM].name.name};" + case Some((out,_,_)) => s"$storeO^$tId = $out;" + }) + EquipmentModel( + x.id.name, + phylogInitiatorFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + loadI :: storeO :: Nil, + worst.values.map(_._3).toList, + Nil, + Nil, + s"""assert + |// worst value computation + |${worst.values.flatMap(_._2).toList.distinct.sorted.mkString("\n")} + | + |// impact computation + |${storeAssertions.toList.distinct.sorted.mkString("\n")} + | + |// icon management + |icone = 1; + |""".stripMargin + ) + } + } +} diff --git a/src/main/scala/views/dependability/exporters/SystemCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/SystemCeciliaExporter.scala new file mode 100644 index 0000000..9bea407 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/SystemCeciliaExporter.scala @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.model.{InputDepTarget, System, Variable} + +trait SystemCeciliaExporter { + self: BasicOperationCeciliaExporter & TypeCeciliaExporter => + + implicit def systemIsExportable[T <: System]: Aux[T, SystemModel] = new CeciliaExporter[T] { + type R = SystemModel + + def toCecilia(x: T): SystemModel = { + val targets = x.context.portOwner.values.collect{case t:InputDepTarget[_] => t}.toSet + val epsilons = targets.map{t => DeterministicEventModel(Symbol(s"$t.${t.fMAutomaton}.${t.fMAutomaton.epsilon}"))} + val fires = targets.toList.sortBy(_.id.name).map{t => s"$t.fire"} + val directions = targets.toList.sortBy(_.id.name).map{t => s"$t.direction"} + val scheduler = WorstSchedulerTopHelper(epsilons.size) + val schedulerModel = SubComponent(Symbol("worstScheduler"), scheduler.model) + val topLevelPort = (v: Variable[_]) => !x.context.componentOwner.isDefinedAt(x.context.portOwner(v.id)) + val topLevelConnections = x.context.links.collect { + case (i, s) if topLevelPort(i) => + if (s.size != 1) + s.zipWithIndex.map(p => s"${variablePathName(x, i)}${p._2} = ${variablePathName(x, p._1)};") + else + Set(s"${variablePathName(x, i)} = ${variablePathName(x, s.head)};") + }.flatten + SystemModel( + x.name, + PhylogFolder.phylogSystemExampleFolder, + x.context.toBuild.values.map(f => f()).toList :+ schedulerModel, + Nil, + SynchroEventModel(Symbol("epsilon"),epsilons.toList,"mec") :: Nil, + Nil, + s"""assert + |//component connections + |${topLevelConnections.toList.sorted.mkString("\n")} + | + |//target evolution management + |${fires.zip(scheduler.fireOrders).map(p => s"${p._1} = $schedulerModel.${p._2};").mkString("\n")} + |${scheduler.sonDirection.zip(directions).map(p => s"$schedulerModel.${p._1} = ${p._2};").mkString("\n")} + |""".stripMargin + ) + } + } +} diff --git a/src/main/scala/views/dependability/exporters/TargetCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/TargetCeciliaExporter.scala new file mode 100644 index 0000000..0d304de --- /dev/null +++ b/src/main/scala/views/dependability/exporters/TargetCeciliaExporter.scala @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.exporters.GenericImage._ +import views.dependability.exporters.PhylogFolder.phylogTargetFolder +import views.dependability.model._ +import views.dependability.operators._ + +trait TargetCeciliaExporter { + self:TypeCeciliaExporter with AutomatonCeciliaExporter with BasicOperationCeciliaExporter => + + implicit def inputDepTargetExporter[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering]: Aux[InputDepTarget[FM], EquipmentModel] = new CeciliaExporter[InputDepTarget[FM]] { + type R = EquipmentModel + + def toCecilia(x: InputDepTarget[FM]): EquipmentModel = { + val tyype = typeModel[(InitiatorId, TargetId), FM] + val iTyype = typeModel[InitiatorId,FM] + val in = mkVariableName(x.storeI.id, tyype, In, x.storeI.eval().get.size) + val out = Flow(x.loadO.id.name, tyype, Out) + val obs = Flow(Symbol("storeO"),iTyype,Out) + val direction = Flow(Symbol("direction"), typeModel[Direction.Value], Out) + val fire = Flow(Symbol("fire"),typeModel[Fire.Value], In) + val componentExport = x.fMAutomaton.toCecilia + val automaton = SubComponent(x.fMAutomaton.id.name, componentExport) + val myFields = allOf[(InitiatorId, TargetId)].collect{ + case p@(_,t) if t == x.id => p.name.name + } + val otherFields = allOf[(InitiatorId, TargetId)].collect{ + case p@(_,t) if t != x.id => p.name.name + } + val (worstFlow, worstAssertions, worstSub) = mkWorstSub(myFields.flatMap{f => in.map(i => s"$i^$f")}.toSet) + val inAssertion = + s"$automaton.${x.fMAutomaton.in.id} = $worstFlow;" + val outAssertions = + myFields.map(f => s"$out^$f = $automaton.${x.fMAutomaton.o.id};") ++ + otherFields.map(f => s"$out^$f = ${noneOf[FM].name.name};") + val worstByIni = allOf[InitiatorId].map(f => f -> mkWorstSub(in.map(i => s"$i^${(f,x.id).name.name}").toSet)).toMap + val obsSub = + allOf[InitiatorId].map(f => f -> + mkContainerShadowSub(Symbol(s"shadowStore$f"),s"${worstByIni(f)._1}",s"$automaton.${x.fMAutomaton.o}")).toMap + EquipmentModel( + x.id.name, + phylogTargetFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + in :+ out :+ direction :+ fire :+ obs, + obsSub.values.map(_._3).toList ++ worstByIni.values.map(_._3) :+ automaton :+ worstSub, + Nil, + Nil, + s"""assert + |// computing worst store on target + |${worstAssertions.sorted.mkString("\n")} + |$inAssertion + | + |// automaton evolution management + |$direction = $automaton.$direction; + |$automaton.$fire = $fire; + | + |// load status according to component state + |${outAssertions.sorted.mkString("\n")} + | + |// computing store status from component state + |${worstByIni.values.flatMap(_._2).toList.distinct.sorted.mkString("\n")} + |${obsSub.values.flatMap(_._2).toList.distinct.sorted.mkString("\n")} + | + |// observator of store states according to component state + |${obsSub.map(p => s"$obs^${p._1} = ${p._2._1};").toList.sorted.mkString("\n")} + | + |// icone management + |icone = (if $automaton.${x.fMAutomaton.o.id} = ${min[FM].name.name} then 1 else 2); + """.stripMargin + ) + } + } + + implicit def inputInDepTargetIsExportable[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering]: Aux[InputInDepTarget[FM], EquipmentModel] = new CeciliaExporter[InputInDepTarget[FM]] { + type R = EquipmentModel + + def toCecilia(x: InputInDepTarget[FM]): EquipmentModel = { + val tyype = typeModel[(InitiatorId, TargetId), FM] + val iTyype = typeModel[InitiatorId,FM] + val in = Flow(x.storeI.id.name, tyype, In) + val out = Flow(x.loadO.id.name, tyype, Out) + val obs = Flow(Symbol("storeO"),iTyype,Out) + val componentExport = x.fMAutomaton.toCecilia + val sub = SubComponent(x.fMAutomaton.id.name, componentExport) + val myFields = allOf[(InitiatorId, TargetId)].collect{ + case p@(_,t) if t == x.id => p.name.name + } + val otherFields = allOf[(InitiatorId, TargetId)].collect{ + case p@(_,t) if t != x.id => p.name.name + } + val outAssertions = myFields.map(f => s"$out^$f = $sub.${x.fMAutomaton.o.id};") ++ + otherFields.map(f => s"$out^$f = ${noneOf[FM].name.name};") + val obsSub = + allOf[InitiatorId].map(f => f -> + mkContainerShadowSub(Symbol(s"shadowStore$f"),s"$in^${(f,x.id).name.name}",s"$sub.${x.fMAutomaton.o}")).toMap + + EquipmentModel( + x.id.name, + phylogTargetFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + in :: out :: obs :: Nil, + obsSub.values.map(_._3).toList :+ sub, + Nil, + Nil, + s"""assert + |// subcomponent assertions + |${obsSub.values.flatMap(_._2).toList.distinct.sorted.mkString("\n")} + | + |// resulting store assertions + |${obsSub.map(p => s"$obs^${p._1} = ${p._2._1};").toList.sorted.mkString("\n")} + | + |// load status according to component state + |${outAssertions.sorted.mkString("\n")} + | + | + |//icon management + |icone = (if $sub.${x.fMAutomaton.o.id} = ${min[FM]} then 1 else 2); + """.stripMargin + ) + } + } +} diff --git a/src/main/scala/views/dependability/exporters/TransporterCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/TransporterCeciliaExporter.scala new file mode 100644 index 0000000..f87f7d7 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/TransporterCeciliaExporter.scala @@ -0,0 +1,281 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.exporters.CeciliaExporter.Aux +import views.dependability.exporters.GenericImage._ +import views.dependability.exporters.PhylogFolder.phylogTransporterFolder +import views.dependability.model._ +import views.dependability.operators._ + +trait TransporterCeciliaExporter { + self:TypeCeciliaExporter with AutomatonCeciliaExporter with BasicOperationCeciliaExporter => + + implicit def simpleTransporterIsExportable[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering]: Aux[SimpleTransporter[FM], EquipmentModel] = new CeciliaExporter[SimpleTransporter[FM]] { + type R = EquipmentModel + + def toCecilia(x: SimpleTransporter[FM]): EquipmentModel = { + val requestType = typeModel[(InitiatorId, TargetId), FM] + val requestI = mkVariableName(x.storeI.id, requestType, In, x.storeI.eval().get.size) //TODO dangerous + val accessI = mkVariableName(x.loadI.id, requestType, In, x.loadI.eval().get.size) //TODO dangerous + val componentExport = x.fMAutomaton.toCecilia + val automaton = SubComponent(x.fMAutomaton.id.name, componentExport) //TODO Placement + val subStateName = s"$automaton.${x.fMAutomaton.o.id}" + val requestShadowMap = requestType.fields.map(field => field -> + requestI.map(r => mkContainerShadowSub(Symbol(s"shadowRequest$r$field"),s"$r^$field",s"$subStateName"))).toMap + val accessShadowMap = requestType.fields.map(field => field -> + accessI.map(r => mkContainerShadowSub(Symbol(s"shadowAccess$r$field"),s"$r^$field",s"$subStateName"))).toMap + val worstShadowRequestMap = requestType.fields.map(field => field -> + mkWorstSub(requestShadowMap(field).map {_._1}.toSet)).toMap + val worstShadowAccessMap = requestType.fields.map(field => field -> + mkWorstSub(accessShadowMap(field).map {_._1}.toSet)).toMap + val wostRequestMap = requestType.fields.map( field => field -> + mkWorstSub(requestI.map(r => s"$r^$field").toSet) + ).toMap + val wostAccessMap = requestType.fields.map( field => field -> + mkWorstSub(accessI.map(r => s"$r^$field").toSet) + ).toMap + val requestO = Flow(x.storeO.id.name, requestType, Out) + val accessO = Flow(x.loadO.id.name, requestType, Out) + + val rejectMap = allOf[(InitiatorId, TargetId)].map(p => p.name -> x.reject(p)).toMap + + val nonCoruptState = allOf[FM].filterNot(fm => fm.isCorruptingFM || fm == min[FM]) + val requestOAssertions = requestType.fields.sortBy(_.name).map(field => + s"""$requestO^$field = + |${ + if (rejectMap(field.name)) + s" ${noneOf[FM].name.name};" + else + s"""case {${allOf[FM].map(state => s"$subStateName = $state : ${worstShadowRequestMap(field)._1},").mkString("\n")} + |else ${wostRequestMap(field)._1}};""".stripMargin + }""".stripMargin + ) + val accessOAssertions = requestType.fields.sortBy(_.name).map(field => + s"""$accessO^$field = + |${ + if (rejectMap(field.name)) + s" ${noneOf[FM].name.name};" + else + s"""case { ${allOf[FM].map(state => s"$subStateName = $state : ${worstShadowAccessMap(field)._1},").mkString("\n")} + |else ${wostAccessMap(field)._1}};""".stripMargin + }""".stripMargin + ) + + EquipmentModel( + x.id.name, + phylogTransporterFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + requestI ++ accessI :+ requestO :+ accessO, + (requestShadowMap.values ++ accessShadowMap.values).flatMap(_.map {_._3}).toList ++ + (worstShadowAccessMap.values ++ worstShadowRequestMap.values ++ wostAccessMap.values ++ wostRequestMap.values).map{_._3} :+ automaton , + Nil, + Nil, + s"""assert + |// shadowing application on each input field + |${(requestShadowMap.values ++ accessShadowMap.values).flatMap(_.flatMap {_._2}).toList.sorted.mkString("\n")} + | + |// worst among shadowed inputs + |${(worstShadowAccessMap.values ++ worstShadowRequestMap.values).flatMap{_._2}.toList.sorted.mkString("\n")} + | + |// worst among non shadowed inputs + |${(wostAccessMap.values ++ wostRequestMap.values).flatMap{_._2}.toList.sorted.mkString("\n")} + | + |// select store status according to component state + |${requestOAssertions.sorted.mkString("\n")} + | + |// select load status according to component state + |${accessOAssertions.sorted.mkString("\n")} + | + |// icone management + |icone = (if $automaton.${x.fMAutomaton.o.id} = ${min[FM].name.name} then 1 else 2); + """.stripMargin + ) + } + } + + + implicit def VirtualizerIsExportable[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering]: Aux[Virtualizer[FM], EquipmentModel] = new CeciliaExporter[Virtualizer[FM]] { + type R = EquipmentModel + + def toCecilia(x: Virtualizer[FM]): EquipmentModel = { + val requestType = typeModel[(InitiatorId, TargetId), FM] + val requestI = mkVariableName(x.storeI.id, requestType, In, x.storeI.eval().get.size) //TODO dangerous + val accessI = mkVariableName(x.loadI.id, requestType, In, x.loadI.eval().get.size) //TODO dangerous + val componentExport = x.fMAutomaton.toCecilia + val automaton = SubComponent(x.fMAutomaton.id.name, componentExport) //TODO Placement + val subStateName = s"$automaton.${x.fMAutomaton.o.id}" + val requestShadowMap = requestType.fields.map(field => field -> + requestI.map(r => mkContainerShadowSub(Symbol(s"shadowRequest$r$field"),s"$r^$field",s"$subStateName"))).toMap + val accessShadowMap = requestType.fields.map(field => field -> + accessI.map(r => mkContainerShadowSub(Symbol(s"shadowAccess$r$field"),s"$r^$field",s"$subStateName"))).toMap + val worstShadowRequestMap = requestType.fields.map(field => field -> + mkWorstSub(requestShadowMap(field).map {_._1}.toSet)).toMap + val worstShadowAccessMap = requestType.fields.map(field => field -> + mkWorstSub(accessShadowMap(field).map {_._1}.toSet)).toMap + val wostRequestMap = requestType.fields.map( field => field -> + mkWorstSub(requestI.map(r => s"$r^$field").toSet) + ).toMap + val wostAccessMap = requestType.fields.map( field => field -> + mkWorstSub(accessI.map(r => s"$r^$field").toSet) + ).toMap + val requestO = Flow(x.storeO.id.name, requestType, Out) + val accessO = Flow(x.loadO.id.name, requestType, Out) + + val rejectMap = allOf[(InitiatorId, TargetId)].map(p => p.name -> x.reject(p)).toMap + + val nonCoruptState = allOf[FM].filterNot(fm => fm.isCorruptingFM || fm == min[FM]) + val requestOAssertions = requestType.fields.sortBy(_.name).map(field => + s"""$requestO^$field = case { + |${allOf[FM].filter(_.isCorruptingFM).map(fm => s"$subStateName = $fm").mkString(" or ")} : $subStateName, + |${ + if (rejectMap(field.name)) + s"else ${noneOf[FM].name.name}" + else + s"""${nonCoruptState.map(state => s"$subStateName = $state : ${worstShadowRequestMap(field)._1},").mkString("\n")} + |else ${wostRequestMap(field)._1}""".stripMargin + } + |};""".stripMargin + ) + val accessOAssertions = requestType.fields.sortBy(_.name).map(field => + s"""$accessO^$field = case { + |${allOf[FM].filter(_.isCorruptingFM).map(fm => s"$subStateName = $fm").mkString(" or ")} : $subStateName, + |${ + if (rejectMap(field.name)) + s"else ${noneOf[FM].name.name}" + else + s"""${nonCoruptState.map(state => s"$subStateName = $state : ${worstShadowAccessMap(field)._1},").mkString("\n")} + |else ${wostAccessMap(field)._1}""".stripMargin + } + |};""".stripMargin + ) + + EquipmentModel( + x.id.name, + phylogTransporterFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + requestI ++ accessI :+ requestO :+ accessO, + (requestShadowMap.values ++ accessShadowMap.values).flatMap(_.map {_._3}).toList ++ + (worstShadowAccessMap.values ++ worstShadowRequestMap.values ++ wostAccessMap.values ++ wostRequestMap.values).map{_._3} :+ automaton , + Nil, + Nil, + s"""assert + |// shadowing application on each input field + |${(requestShadowMap.values ++ accessShadowMap.values).flatMap(_.flatMap {_._2}).toList.sorted.mkString("\n")} + | + |// worst among shadowed inputs + |${(worstShadowAccessMap.values ++ worstShadowRequestMap.values).flatMap{_._2}.toList.sorted.mkString("\n")} + | + |// worst among non shadowed inputs + |${(wostAccessMap.values ++ wostRequestMap.values).flatMap{_._2}.toList.sorted.mkString("\n")} + | + |// select store status according to component state + |${requestOAssertions.sorted.mkString("\n")} + | + |// select load status according to component state + |${accessOAssertions.sorted.mkString("\n")} + | + |// icone management + |icone = (if $automaton.${x.fMAutomaton.o.id} = ${min[FM].name.name} then 1 else 2); + """.stripMargin + ) + } + } + + implicit def initiatorIsExportable[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering]: Aux[Initiator[FM], EquipmentModel] = new CeciliaExporter[Initiator[FM]] { + type R = EquipmentModel + + def toCecilia(x: Initiator[FM]): EquipmentModel = { + val requestType = typeModel[(InitiatorId, TargetId), FM] + val tgtStatusType = typeModel[TargetId, FM] + val requestI = mkVariableName(x.storeI.id, tgtStatusType, In, x.storeI.eval().get.size) //TODO dangerous + val accessI = mkVariableName(x.loadI.id, requestType, In, x.loadI.eval().get.size) //TODO dangerous + val componentExport = x.fMAutomaton.toCecilia + val automaton = SubComponent(x.fMAutomaton.id.name, componentExport) //TODO Placement + val subStateName = s"$automaton.${x.fMAutomaton.o.id}" + val requestShadowMap = allOf[TargetId].map(field => field -> + requestI.map(r => mkContainerShadowSub(Symbol(s"shadowRequest$r$field"),s"$r^$field",s"$subStateName"))).toMap + val accessShadowMap = allOf[TargetId].map(field => field -> + accessI.map(r => mkContainerShadowSub(Symbol(s"shadowAccess$r$field"),s"$r^${(x.id,field).name.name}",s"$subStateName"))).toMap + val worstShadowRequestMap = allOf[TargetId].map(field => field -> + mkWorstSub(requestShadowMap(field).map {_._1}.toSet)).toMap + val worstShadowAccessMap = allOf[TargetId].map(field => field -> + mkWorstSub(accessShadowMap(field).map {_._1}.toSet)).toMap + val wostRequestMap = allOf[TargetId].map( field => field -> + mkWorstSub(requestI.map(r => s"$r^$field").toSet) + ).toMap + val wostAccessMap = allOf[TargetId].map( field => field -> + mkWorstSub(accessI.map(r => s"$r^${{(x.id,field).name.name}}").toSet) + ).toMap + val requestO = Flow(x.storeO.id.name, requestType, Out) + val accessO = Flow(x.loadO.id.name, tgtStatusType, Out) + + val nonCoruptState = allOf[FM].filterNot(fm => fm.isCorruptingFM || fm == min[FM]) + val requestOAssertions = allOf[(InitiatorId, TargetId)].sortBy(_.name).map(field => + if(field._1 != x.id) + s"$requestO^${field.name.name} = ${noneOf[FM]};" + else + s"""$requestO^${field.name.name} = case { + |${allOf[FM].filter(_.isCorruptingFM).map(fm => s"$subStateName = $fm").mkString(" or ")} : $subStateName, + |${nonCoruptState.map(state => s"$subStateName = $state : ${worstShadowRequestMap(field._2)._1},").mkString("\n")} + |else ${wostRequestMap(field._2)._1} + |};""".stripMargin + ) + val accessOAssertions = allOf[TargetId].sortBy(_.name).map(field => + s"""$accessO^$field = case { + |${allOf[FM].filter(_.isCorruptingFM).map(fm => s"$subStateName = $fm").mkString(" or ")} : $subStateName, + |${nonCoruptState.map(state => s"$subStateName = $state : ${worstShadowAccessMap(field)._1},").mkString("\n")} + |else ${wostAccessMap(field)._1} + |};""".stripMargin + ) + + EquipmentModel( + x.id.name, + phylogTransporterFolder, + phylogBlockBlue, + phylogBlockGreen :: phylogBlockRed :: Nil, + requestI ++ accessI :+ requestO :+ accessO, + (requestShadowMap.values ++ accessShadowMap.values).flatMap(_.map {_._3}).toList ++ + (worstShadowAccessMap.values ++ worstShadowRequestMap.values ++ wostAccessMap.values ++ wostRequestMap.values).map{_._3} :+ automaton , + Nil, + Nil, + s"""assert + |// shadowing application on each input field + |${(requestShadowMap.values ++ accessShadowMap.values).flatMap(_.flatMap {_._2}).toList.distinct.sorted.mkString("\n")} + | + |// worst among shadowed inputs + |${(worstShadowAccessMap.values ++ worstShadowRequestMap.values).flatMap{_._2}.toList.distinct.sorted.mkString("\n")} + | + |// worst among non shadowed inputs + |${(wostAccessMap.values ++ wostRequestMap.values).flatMap{_._2}.toList.distinct.sorted.mkString("\n")} + | + |// select store status according to component state + |${requestOAssertions.distinct.sorted.mkString("\n")} + | + |// select load status according to component state + |${accessOAssertions.distinct.sorted.mkString("\n")} + | + |// icone management + |icone = (if $automaton.${x.fMAutomaton.o.id} = ${min[FM].name.name} then 1 else 2); + """.stripMargin + ) + } + } +} diff --git a/src/main/scala/views/dependability/exporters/TypeCeciliaExporter.scala b/src/main/scala/views/dependability/exporters/TypeCeciliaExporter.scala new file mode 100644 index 0000000..27326a7 --- /dev/null +++ b/src/main/scala/views/dependability/exporters/TypeCeciliaExporter.scala @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.exporters + +import views.dependability.operators.{IsFinite, _} + +trait TypeCeciliaExporter { + def typeModel[T: IsFinite]: EnumeratedType = { + EnumeratedType(nameOf[T], allWithNone.map(fm => fm.name).toList, PhylogFolder.phylogFMTypeFolder) + } + + def typeModel[K: IsFinite, V: IsFinite]: RecordType = { + val tyypeMode = typeModel[V] + RecordType(Symbol(s"Map${nameOf[K].name}To${nameOf[V].name}"), PhylogFolder.phylogCustomTypeFolder, allOf[K].map(t => Flow(t.name, tyypeMode, In)).toList) + } + + def boolMapTypeModel[K: IsFinite]: RecordType = { + RecordType(Symbol(s"Map${nameOf[K].name}ToBool"), PhylogFolder.phylogCustomTypeFolder, allOf[K].map(t => Flow(t.name, CeciliaBoolean, In)).toList) + } +} diff --git a/src/main/scala/views/dependability/exporters/package.scala b/src/main/scala/views/dependability/exporters/package.scala new file mode 100644 index 0000000..990e60a --- /dev/null +++ b/src/main/scala/views/dependability/exporters/package.scala @@ -0,0 +1,12 @@ +package views.dependability + +package object exporters extends TypeCeciliaExporter + with BasicOperationCeciliaExporter + with AutomatonCeciliaExporter + with TargetCeciliaExporter + with TransporterCeciliaExporter + with SoftwareCeciliaExporter + with SystemCeciliaExporter + with PlatformCeciliaExporter + with CeciliaExporterOps + with ExprCeciliaExporter diff --git a/src/main/scala/views/dependability/model/Component.scala b/src/main/scala/views/dependability/model/Component.scala new file mode 100644 index 0000000..4a17f8e --- /dev/null +++ b/src/main/scala/views/dependability/model/Component.scala @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +trait Component { + val id:Id + + override def toString: String = id.name.name +} diff --git a/src/main/scala/views/dependability/model/Copy.scala b/src/main/scala/views/dependability/model/Copy.scala new file mode 100644 index 0000000..699f487 --- /dev/null +++ b/src/main/scala/views/dependability/model/Copy.scala @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +case class Copy(targetNeeded:Set[TargetId], targetWritten:Set[TargetId]) diff --git a/src/main/scala/views/dependability/model/CustomTypes.scala b/src/main/scala/views/dependability/model/CustomTypes.scala new file mode 100644 index 0000000..d30fe25 --- /dev/null +++ b/src/main/scala/views/dependability/model/CustomTypes.scala @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.operators._ + +object CustomTypes { + + type Request[T] = Map[(InitiatorId,TargetId),T] + + object Request { + def empty[T] = Map.empty[(InitiatorId,TargetId),T] + def all[T](fm:T) : Request[T] = Map(allOf[InitiatorId].flatMap(sID => allOf[TargetId].map{tId => (sID,tId)}).map(_ -> fm):_*) + def apply[T](l:((InitiatorId,TargetId),T)*) : Request[T] = l.toMap + } + + type TargetStatus[T] = Map[TargetId,T] + + object TargetStatus { + def empty[T] = Map.empty[TargetId,T] + def all[T](fm:T) : TargetStatus[T] = allOf[TargetId].map{_ -> fm}.toMap + def apply[T](l:(TargetId,T)*) : TargetStatus[T] = l.toMap + } + +} diff --git a/src/main/scala/views/dependability/model/Direction.scala b/src/main/scala/views/dependability/model/Direction.scala new file mode 100644 index 0000000..ca3c68c --- /dev/null +++ b/src/main/scala/views/dependability/model/Direction.scala @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.operators.{IsCriticityOrdering, IsFinite} + +object Direction extends Enumeration { + + val Degradation: Value = Value(3, "degradation") + val Reparation: Value = Value(2,"reparation") + val Constant: Value = Value(1,"constant") + + + implicit val isFinite: IsFinite[Value] = new IsFinite[Value] { + val none: Value = Constant + def allWithNone: Seq[Value] = values.toSeq + def name(x: Value): Symbol = Symbol(x.toString) + } + + implicit val isCriticityOrdering : IsCriticityOrdering[Value] = (x: Direction.Value, y: Direction.Value) => x.id - y.id + +} diff --git a/src/main/scala/views/dependability/model/EnumFailureMode.scala b/src/main/scala/views/dependability/model/EnumFailureMode.scala new file mode 100644 index 0000000..9b26fab --- /dev/null +++ b/src/main/scala/views/dependability/model/EnumFailureMode.scala @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.operators.{IsCriticityOrdering, IsFinite, IsShadowOrdering} + +trait EnumFailureMode extends Enumeration { + self => + + implicit object criticityOrdering extends IsCriticityOrdering[Value] { + def compare(x: Value, y: Value): Int = x.id - y.id + } + + implicit val isFinite: IsFinite[Value] + + implicit object isShadowOrdering extends IsShadowOrdering[Value] { + def containerShadow(init: Value, containerState: Value): Value = self.containerShadow(init, containerState) + + def corruptingFM(fm: Value): Boolean = self.corruptingFM(fm) + + def inputShadow(input: Value, containerState: Value): Value = self.inputShadow(input, containerState) + } + + def containerShadow(init: Value, containerFM: Value): Value + + def corruptingFM(fm: Value): Boolean + + def inputShadow(input: Value, containerState: Value): Value +} diff --git a/src/main/scala/views/dependability/model/Event.scala b/src/main/scala/views/dependability/model/Event.scala new file mode 100644 index 0000000..9c95049 --- /dev/null +++ b/src/main/scala/views/dependability/model/Event.scala @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.executor.Simulator + +trait Event { + val name:Symbol + override def toString: String = name.name +} + +case class SynchroEvent(name:Symbol, synchronizedEvents: Set[Event]) extends Event + +trait ConcreteEvent extends Event { + val owner: ModeAutomaton[_] +} + +case class StochasticEvent[T](name:Symbol, owner:ModeAutomaton[T]) extends ConcreteEvent + +object StochasticEvent{ + def apply[T](name: Symbol, owner: ModeAutomaton[T]): StochasticEvent[T] = { + val r = new StochasticEvent[T](name, owner) + Simulator.addEvent(r) + r + } +} + +case class DetermisticEvent[T](name:Symbol, owner:ModeAutomaton[T], delay:Int) extends ConcreteEvent { + override def toString: String = name.name +} + +object DetermisticEvent{ + def apply[T](name: Symbol, owner: ModeAutomaton[T], delay:Int): DetermisticEvent[T] = { + val r = new DetermisticEvent[T](name, owner,delay) + Simulator.addEvent(r) + r + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/model/Fire.scala b/src/main/scala/views/dependability/model/Fire.scala new file mode 100644 index 0000000..4b1c0bc --- /dev/null +++ b/src/main/scala/views/dependability/model/Fire.scala @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.operators.IsFinite + +object Fire extends Enumeration { + + val Apply: Value = Value(3,"apply") + val Wait: Value = Value(2, "wait") + val No: Value = Value(1,"no") + + + implicit val isFinite: IsFinite[Value] = new IsFinite[Value] { + val none: Value = No + def allWithNone: Seq[Value] = values.toSeq + def name(x: Value): Symbol = Symbol(x.toString) + } + +} diff --git a/src/main/scala/views/dependability/model/Id.scala b/src/main/scala/views/dependability/model/Id.scala new file mode 100644 index 0000000..d62c482 --- /dev/null +++ b/src/main/scala/views/dependability/model/Id.scala @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.operators.IsFinite + +import scala.collection.mutable + +trait Id{ + val name : Symbol + override def toString: String = name.name +} + +trait IdBuilder[T<:Id]{ + val noneId : T + val tyypeName : Symbol + protected val _memo = mutable.HashMap.empty[Symbol,T] + implicit object TIsFinite extends IsFinite[T] { + val none: T = noneId + override def typeName: Symbol = tyypeName + def allWithNone: Seq[T] = _memo.values.toSeq :+ noneId + def name(x: T): Symbol = x.name + } +} + +case class VariableId private(name:Symbol) extends Id + +object VariableId extends IdBuilder[VariableId]{ + val noneId = new VariableId(Symbol("none")) + val tyypeName: Symbol = Symbol("VariableId") + def apply(name: Symbol): VariableId = _memo.getOrElseUpdate(name, new VariableId(name)) +} + +class TargetId private(val name:Symbol) extends Id { + override def toString: String = name.name +} + +object TargetId extends IdBuilder[TargetId]{ + val noneId = new TargetId(Symbol("none")) + val tyypeName: Symbol = Symbol("TargetId") + def apply(name: Symbol): TargetId = _memo.getOrElseUpdate(name, new TargetId(name)) +} + +case class SoftwareId private(name:Symbol) extends Id { + override def toString: String = name.name +} + +object SoftwareId extends IdBuilder[SoftwareId]{ + val noneId = new SoftwareId(Symbol("none")) + val tyypeName: Symbol = Symbol("SoftwareId") + def apply(name: Symbol): SoftwareId = _memo.getOrElseUpdate(name, new SoftwareId(name)) +} + +case class TransporterId(name:Symbol) extends Id { + override def toString: String = name.name +} + +case class InitiatorId private(name:Symbol) extends Id { + override def toString: String = name.name +} + +object InitiatorId extends IdBuilder[InitiatorId]{ + val noneId = new InitiatorId(Symbol("none")) + val tyypeName: Symbol = Symbol("InitiatorId") + def apply(name: Symbol): InitiatorId = _memo.getOrElseUpdate(name, new InitiatorId(name)) +} + +case class AutomatonId(name:Symbol) extends Id \ No newline at end of file diff --git a/src/main/scala/views/dependability/model/ModeAutomaton.scala b/src/main/scala/views/dependability/model/ModeAutomaton.scala new file mode 100644 index 0000000..72dee98 --- /dev/null +++ b/src/main/scala/views/dependability/model/ModeAutomaton.scala @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.model.Direction.{Constant, Degradation, Reparation} +import views.dependability.operators._ + +/** + * Base trait for all automaton-like component + * + * @tparam T Possible modes of the automaton + */ +abstract class ModeAutomaton[T: IsCriticityOrdering : IsFinite] extends Component { + val events: Set[Event] + val transitions: Set[Transition[T]] + val initialState: T + private var state: T = initialState + private var nextState: Option[T] = None + + def getState: T = state + + def fire(e: Event): Unit = + fireable(e) match { + case None => throw new Exception(s"$e is not fireable") + case Some(t) => state = t.computeNewState() + } + + def engage(e: Event): Unit = for (t <- fireable(e)) yield nextState = Some(t.computeNewState()) + + def update(): Unit = for (s <- nextState) yield state = s + + def fireable(e: Event): Option[Transition[T]] = { + transitions.filter(t => t.e == e && t.guard()) match { + case s if s.isEmpty => None + case s if s.size == 1 => Some(s.head) + case s => throw new Exception(s"non deterministic automaton, $s are available for $e") + } + } + + def direction(e: Event): Option[Direction.Value] = { + for (t <- fireable(e); nextState = t.computeNewState()) yield { + if (nextState == state) + Constant + else if (nextState < state) + Reparation + else + Degradation + } + } +} + +abstract class FMAutomaton[T: IsCriticityOrdering : IsFinite] extends ModeAutomaton[T] { + val initialState: T + val eventMap: Map[Symbol, Event] + val o: OutputPort[T] + val outputPorts: Set[OutputPort[T]] + val transitions: Set[Transition[T]] + val id: AutomatonId +} + +class SimpleFMAutomaton[T: IsCriticityOrdering : IsFinite] private(val id: AutomatonId, val initialState: T) extends FMAutomaton[T] { + val events: Set[Event] = allWithNone[T].map(x => StochasticEvent(x.name, this)).toSet + val eventMap: Map[Symbol, Event] = events.map(e => e.name -> e).toMap + val transitions: Set[Transition[T]] = + allWithNone[T].map(to => + Transition( + () => getState < to, + eventMap(to.name), + () => to + ) + ).toSet + val o: OutputPort[T] = OutputPort(VariableId(Symbol("o")), () => Some(getState)) + val outputPorts: Set[OutputPort[T]] = Set(o) +} + +object SimpleFMAutomaton { + def apply[T: IsCriticityOrdering : IsFinite](id: AutomatonId, initialState: T)(implicit owner: Owner): SimpleFMAutomaton[T] = { + val r = new SimpleFMAutomaton[T](id, initialState) + owner.portOwner(r.o.id) = r + r + } +} + + +class InputFMAutomaton[T: IsCriticityOrdering : IsFinite : IsShadowOrdering](val id: AutomatonId, val initialState: T) extends FMAutomaton[T] { + val in: InputPort[T] = InputPort(Symbol("i")) + val inputPorts: Set[InputPort[T]] = Set(in) + val epsilon: DetermisticEvent[T] = DetermisticEvent(Symbol("espilon"), this, 0) + val events: Set[Event] = allWithNone[T].map(x => StochasticEvent(x.name, this)).toSet[Event] + epsilon + val eventMap: Map[Symbol, Event] = events.map(e => e.name -> e).toMap + val transitions: Set[Transition[T]] = + allWithNone[T].map(to => + Transition( + () => getState < to, + eventMap(to.name), + () => to) + ).toSet + + Transition( + () => (for (i <- in.eval()) yield i.inputShadow(getState) != getState) getOrElse false, + epsilon, + () => in.eval().get.inputShadow(getState) + ) + val o: OutputPort[T] = OutputPort(VariableId(Symbol("o")), () => Some(getState)) + val outputPorts: Set[OutputPort[T]] = Set(o) +} + +object InputFMAutomaton { + def apply[T: IsCriticityOrdering : IsFinite : IsShadowOrdering](id: AutomatonId, initialState: T)(implicit owner: Owner): InputFMAutomaton[T] = { + val r = new InputFMAutomaton[T](id, initialState) + owner.portOwner(r.o.id) = r + owner.portOwner(r.in.id) = r + r + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/model/Software.scala b/src/main/scala/views/dependability/model/Software.scala new file mode 100644 index 0000000..3a706e7 --- /dev/null +++ b/src/main/scala/views/dependability/model/Software.scala @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.exporters._ +import views.dependability.model.CustomTypes.TargetStatus +import views.dependability.operators._ + + +trait Software[FM] extends Component { + val id: SoftwareId + val loadI: InputPort[TargetStatus[FM]] = InputPort[TargetStatus[FM]](Symbol("loadI")) + val storeO: OutputPort[TargetStatus[FM]] +} + +/** + * + * @param id name of the software + * @param softwareState core's state and loads impact on software state + * @param stores core's state and stores impact on software stores + * @tparam FM type of the failure modes + */ +class Application[FM: IsCriticityOrdering : IsFinite] private( + val id: SoftwareId, + val softwareState: (Variable[FM], Variable[TargetStatus[FM]]) => Expr[FM], + val stores: (Variable[FM], Variable[TargetStatus[FM]]) => Expr[TargetStatus[FM]]) extends Software[FM] { + val coreState: InputPort[FM] = InputPort(Symbol("coreState")) + val storeO: OutputPort[TargetStatus[FM]] = + OutputPort(VariableId(Symbol("storeO")), () => stores(coreState, loadI).eval()) + val stateO: OutputPort[FM] = + OutputPort(VariableId(Symbol("stateO")), () => softwareState(coreState, loadI).eval()) +} + +object Application { + def apply[FM: IsCriticityOrdering : IsFinite]( + id: SoftwareId, + softwareState: (Variable[FM], Variable[TargetStatus[FM]]) => Expr[FM], + stores: (Variable[FM], Variable[TargetStatus[FM]]) => Expr[TargetStatus[FM]])(implicit ev: Builder[SubComponent] with Owner): Application[FM] = { + val result = new Application(id, softwareState, stores) + ev.portOwner(result.coreState.id) = result + ev.portOwner(result.storeO.id) = result + ev.portOwner(result.loadI.id) = result + ev.portOwner(result.stateO.id) = result + ev.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + result + } +} + +class Descriptor[FM: IsCriticityOrdering : IsFinite] private(val id: SoftwareId, val transferts: List[Copy])(implicit owner: Owner) extends Software[FM] { + val storeO: OutputPort[TargetStatus[FM]] = OutputPort(VariableId(Symbol("requestO")), () => { + for (loadStatus <- loadI.eval()) yield { + val copies = transferts.map(t => + t -> worst( + (t.targetNeeded.map( + loadStatus + )).toSeq: _*) //TODO Raise error when the status cannot be computed => connection error + ) + val reqs = copies.map(p => TargetStatus(p._1.targetWritten.map(_ -> p._2).toSeq: _*)) + reqs.foldLeft(TargetStatus.empty[FM])((acc, r) => { + acc.mergeWith(r, (a, b) => worst(a, b)) + }) + } + }) +} + +object Descriptor { + def apply[FM: IsCriticityOrdering : IsFinite](id: SoftwareId, transferts: List[Copy])(implicit context: Builder[SubComponent] with Owner): Descriptor[FM] = { + val result = new Descriptor(id, transferts) + context.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + context.portOwner(result.loadI.id) = result + context.portOwner(result.storeO.id) = result + result + } +} + diff --git a/src/main/scala/views/dependability/model/System.scala b/src/main/scala/views/dependability/model/System.scala new file mode 100644 index 0000000..ae1c8fc --- /dev/null +++ b/src/main/scala/views/dependability/model/System.scala @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.exporters.SubComponent + +import scala.collection.mutable + +trait Builder[T]{ + val toBuild: mutable.HashMap[Symbol, () => T] = mutable.HashMap.empty +} + +trait Linker{ + val links : mutable.HashMap[Variable[_], Set[Variable[_]]] = mutable.HashMap.empty +} + +trait Owner { + val portOwner : mutable.HashMap[VariableId, Component] = mutable.HashMap.empty + val componentOwner : mutable.HashMap[Component, Component] = mutable.HashMap.empty +} + +case class System(name:Symbol) { + + implicit val context: Builder[SubComponent] with Linker with Owner = new Builder[SubComponent] with Linker with Owner {} + + override def toString: String = name.name + + implicit class listExtensionMethods[T](l:List[OutputPort[T]]) { + def |+|(that : OutputPort[T]) : List[OutputPort[T]] = l :+ that + def |++|(that : List[OutputPort[T]]) : List[OutputPort[T]] = l ++ that + } +} + diff --git a/src/main/scala/views/dependability/model/Target.scala b/src/main/scala/views/dependability/model/Target.scala new file mode 100644 index 0000000..85c4a96 --- /dev/null +++ b/src/main/scala/views/dependability/model/Target.scala @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.exporters.{SubComponent, _} +import views.dependability.model.CustomTypes.Request +import views.dependability.operators._ + +trait Target[FM] extends Component { + val id: TargetId + val storeI: InputPort[List[Request[FM]]] = InputPort[List[Request[FM]]](Symbol("storeI")) + val fMAutomaton: FMAutomaton[FM] + val loadO: OutputPort[Request[FM]] = + OutputPort(VariableId(Symbol("loadO")), () => + for (o <- fMAutomaton.o.eval()) yield Request(allOf[InitiatorId].map(sId => (sId, id) -> o): _*) + ) +} + +class InputDepTarget[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering] private(val id: TargetId)(implicit ev: Owner) extends Target[FM] { + val fMAutomaton: InputFMAutomaton[FM] = InputFMAutomaton[FM](AutomatonId(Symbol("fmAutomaton")), min[FM]) + fMAutomaton.in := { + for (requests <- storeI.eval()) yield { + val stores = requests.flatMap{ r => r.collect { case (k, v) if k._2 == id => v }} + if (stores.isEmpty) + noneOf[FM] + else + worst(stores: _*) + } + } +} + +object InputDepTarget { + + def apply[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering](id: TargetId)(implicit context: Builder[SubComponent] with Owner): InputDepTarget[FM] = { + val result = new InputDepTarget(id) + context.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + context.portOwner(result.loadO.id) = result + context.portOwner(result.storeI.id) = result + context.componentOwner(result.fMAutomaton) = result + result + } +} + +class InputInDepTarget[FM: IsCriticityOrdering : IsFinite] private(val id: TargetId)(implicit owner: Owner) extends Target[FM] { + val fMAutomaton: SimpleFMAutomaton[FM] = SimpleFMAutomaton[FM](AutomatonId(Symbol("fmAutomaton")), min[FM]) +} + +object InputInDepTarget { + def apply[FM: IsCriticityOrdering : IsFinite : IsShadowOrdering](id: TargetId)(implicit context: Builder[SubComponent] with Owner): InputInDepTarget[FM] = { + val result = new InputInDepTarget(id) + context.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + context.portOwner(result.loadO.id) = result + context.portOwner(result.storeI.id) = result + context.componentOwner(result.fMAutomaton) = result + result + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/model/Transition.scala b/src/main/scala/views/dependability/model/Transition.scala new file mode 100644 index 0000000..bd8cfed --- /dev/null +++ b/src/main/scala/views/dependability/model/Transition.scala @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +/** + * Representation of automaton transitions + * @param guard predicate encoding the guard of the transition + * @param e the trigger event + * @param computeNewState the new state when the transaction will be fired + * @tparam T the type of the owner state + */ +case class Transition[T](guard:() => Boolean, e:Event, computeNewState: () => T) diff --git a/src/main/scala/views/dependability/model/Transporter.scala b/src/main/scala/views/dependability/model/Transporter.scala new file mode 100644 index 0000000..8107a13 --- /dev/null +++ b/src/main/scala/views/dependability/model/Transporter.scala @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import views.dependability.exporters._ +import views.dependability.model.CustomTypes.{Request, TargetStatus} +import views.dependability.operators._ + + +abstract class Transporter [FM: IsCriticityOrdering : IsFinite : IsShadowOrdering](implicit owner: Owner) extends Component { + val loadI: InputPort[List[Request[FM]]] = InputPort[List[Request[FM]]](Symbol("loadI")) + val initialState: FM = min[FM] + val fMAutomaton: SimpleFMAutomaton[FM] = SimpleFMAutomaton[FM](AutomatonId(Symbol(s"fmAutomaton")),initialState) + val storeO : OutputPort[Request[FM]] +} + +abstract class BasicTransporter [FM: IsCriticityOrdering : IsFinite : IsShadowOrdering](implicit owner: Owner) extends Transporter[FM] { + val loadO: OutputPort[Request[FM]] + val storeI: InputPort[List[Request[FM]]] +} + +class SimpleTransporter [FM: IsCriticityOrdering : IsFinite : IsShadowOrdering] private(val id : TransporterId,val reject: ((InitiatorId,TargetId)) => Boolean)(implicit owner: Owner) extends BasicTransporter[FM]{ + val storeI: InputPort[List[Request[FM]]] = InputPort[List[Request[FM]]](Symbol("storeI")) + val storeO : OutputPort[Request[FM]] = + OutputPort(VariableId(Symbol("storeO")), () => + for(reqs <- storeI.eval(); s <- fMAutomaton.o.eval()) yield { + reqs.map(r => { + if (s.isCorruptingFM) { + Request.all(s) + }else if (s != min[FM]) { + r.map(kv => kv._1 -> kv._2.containerShadow(s)) + }else { + r.filterNot(p => reject(p._1)) + } + }).foldLeft(Request.empty[FM])((acc,r) => { + acc.mergeWith(r,(a,b) => worst[FM](a,b)) + }) + }) + val loadO : OutputPort[Request[FM]] = + OutputPort(VariableId(Symbol("loadO")), () => + for(access <- loadI.eval(); s <- fMAutomaton.o.eval()) yield { + access.map(a => { + a.map(kv => kv._1 -> kv._2.containerShadow(s)) + }).foldLeft(Request.empty[FM])((acc,r) => { + acc.mergeWith(r,(a,b) => worst[FM](a,b)) + }) + }) +} + +object SimpleTransporter { + def apply[T: IsCriticityOrdering : IsFinite : IsShadowOrdering](id: TransporterId, reject: ((InitiatorId, TargetId)) => Boolean)(implicit context: Builder[SubComponent] with Owner): SimpleTransporter[T] = { + val result = new SimpleTransporter(id, reject) + context.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + context.portOwner(result.loadO.id) = result + context.portOwner(result.storeO.id) = result + context.portOwner(result.loadI.id) = result + context.portOwner(result.storeI.id) = result + context.componentOwner(result.fMAutomaton) = result + result + } +} + +//TODO Refactoring with Simple transporter +class Virtualizer [FM: IsCriticityOrdering : IsFinite : IsShadowOrdering] private(val id : TransporterId,val reject: ((InitiatorId,TargetId)) => Boolean)(implicit owner: Owner) extends BasicTransporter[FM]{ + val storeI: InputPort[List[Request[FM]]] = InputPort[List[Request[FM]]](Symbol("storeI")) + val storeO : OutputPort[Request[FM]] = + OutputPort(VariableId(Symbol("storeO")), () => + for(reqs <- storeI.eval(); s <- fMAutomaton.o.eval()) yield { + reqs.map(r => { + if (s.isCorruptingFM) { + Request.all(s) + }else if (s != min[FM]) { + r.map(kv => kv._1 -> kv._2.containerShadow(s)) + }else { + r.filterNot(p => reject(p._1)) + } + }).foldLeft(Request.empty[FM])((acc,r) => { + acc.mergeWith(r,(a,b) => worst[FM](a,b)) + }) + }) + val loadO : OutputPort[Request[FM]] = + OutputPort(VariableId(Symbol("loadO")), () => + for(access <- loadI.eval(); s <- fMAutomaton.o.eval()) yield { + access.map(a => { + a.map(kv => kv._1 -> kv._2.containerShadow(s)) + }).foldLeft(Request.empty[FM])((acc,r) => { + acc.mergeWith(r,(a,b) => worst[FM](a,b)) + }) + }) +} + +object Virtualizer { + def apply[T: IsCriticityOrdering : IsFinite : IsShadowOrdering](id: TransporterId, reject: ((InitiatorId, TargetId)) => Boolean)(implicit context: Builder[SubComponent] with Owner): Virtualizer[T] = { + val result = new Virtualizer(id, reject) + context.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + context.portOwner(result.loadO.id) = result + context.portOwner(result.storeO.id) = result + context.portOwner(result.loadI.id) = result + context.portOwner(result.storeI.id) = result + context.componentOwner(result.fMAutomaton) = result + result + } +} + +class Initiator [FM: IsCriticityOrdering : IsFinite : IsShadowOrdering] private(val id : InitiatorId)(implicit owner: Owner) extends Transporter[FM]{ + val storeI: InputPort[List[TargetStatus[FM]]] = InputPort[List[TargetStatus[FM]]](Symbol("storeI")) + val storeO : OutputPort[Request[FM]] = + OutputPort(VariableId(Symbol("storeO")), () => + for(reqs <- storeI.eval(); s <- fMAutomaton.o.eval()) yield { + reqs.map(r => { + if (s.isCorruptingFM) { + Request.all(s) + }else if (s != min[FM]) { + r.map(kv => (id,kv._1) -> kv._2.containerShadow(s)) + }else { + r.map(p => (id,p._1) -> p._2) + } + }).foldLeft(Request.empty[FM])((acc,r) => { + acc.mergeWith(r,(a,b) => worst[FM](a,b)) + }) + }) + val loadO : OutputPort[TargetStatus[FM]] = + OutputPort(VariableId(Symbol("loadO")), () => + for(access <- loadI.eval(); s <- fMAutomaton.o.eval()) yield { + access.map(a => { + a.collect({ + case ((i,t),v) if i == id => t -> v.containerShadow(s) + }) + }).foldLeft(TargetStatus.empty[FM])((acc,r) => { + acc.mergeWith(r,(a,b) => worst[FM](a,b)) + }) + }) +} + +object Initiator { + def apply[T: IsCriticityOrdering : IsFinite : IsShadowOrdering](id: InitiatorId)(implicit context: Builder[SubComponent] with Owner): Initiator[T] = { + val result = new Initiator(id) + context.toBuild.getOrElseUpdate(id.name, () => SubComponent(id.name, result.toCecilia)) + context.portOwner(result.loadO.id) = result + context.portOwner(result.storeO.id) = result + context.portOwner(result.loadI.id) = result + context.portOwner(result.storeI.id) = result + context.componentOwner(result.fMAutomaton) = result + result + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/model/Variable.scala b/src/main/scala/views/dependability/model/Variable.scala new file mode 100644 index 0000000..3e84013 --- /dev/null +++ b/src/main/scala/views/dependability/model/Variable.scala @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.model + +import pml.model.hardware.{Target => PMLTarget} +import pml.model.software.Data +import pml.operators.{Used, _} +import scalaz.Leibniz +import views.dependability.operators.{IsCriticityOrdering, IsFinite} + +import scala.language.implicitConversions + +sealed trait Expr[+T] { + def eval(): Option[T] +} + +object ExprImplicits { + implicit def toConst[T](x:T):Const[T]= Const(x) + + implicit def pairToDMap[T](m:(TargetId,Expr[T])) : DMap[T] = DMap(Map(m)) + + implicit def pairSeqToDMap[T](m:Seq[(TargetId,Expr[T])]) : DMap[T] = DMap(Map(m:_*)) + + implicit class OfBuilder[T](m:Variable[Map[TargetId,T]]) { + def fmOf(id:TargetId) : Of[T] = Of(m,id) + def fmOf(t:Target[T]) : Of[T] = Of(m,t.id) + def fmOf(d:Data)(implicit ev : PMLTarget => TargetId, u: Used[Data, PMLTarget]) : Of[T] = + Of(m,ev(d.hostingTargets.head)) //FIXME ERROR IF SEVERAL TARGET FOR DATA + } + implicit class EqualBuilder(e:Expr[_]) { + def === (that:Expr[_]) : Equal = Equal(e,that) + } + implicit class AndBuilder(e:BoolExpr) { + def and (that:BoolExpr) : And = And(e,that) + } + implicit class OrBuilder(e:BoolExpr) { + def and (that:BoolExpr) : Or = Or(e,that) + } + implicit class DataExtension(d:Data) { + def fmIs[T](v:Expr[T])(implicit ev : PMLTarget => TargetId, u: Used[Data, PMLTarget]) : (TargetId,Expr[T]) = + ev(d.hostingTargets.head) -> v + } + implicit class PMLTargetExtension(d:PMLTarget) { + def fmIs[T](v:Expr[T])(implicit ev : PMLTarget => TargetId) : (TargetId,Expr[T]) = ev(d) -> v + def id(implicit ev : PMLTarget => TargetId) : TargetId = ev(d) + } + implicit class TargetIdExtension(d:TargetId) { + def fmIs[T](v:Expr[T]): (TargetId,Expr[T]) = d -> v + } + implicit class TargetExtension[T](d:Target[T]) { + def fmIs(v:Expr[T]): (TargetId,Expr[T]) = d.id -> v + } + case class If[T](b:BoolExpr) { + def Then(t:Expr[T]) : IT[T] = IT(b,t) + } + case class IT[T](b:BoolExpr,t:Expr[T]) { + def Else[U >: T](e: Expr[U]): ITE[U] = ITE(b, t, e) + } +} + +case class ITE[T](i:BoolExpr, t: Expr[T], e:Expr[T]) extends Expr[T]{ + def eval(): Option[T] = for(vi <- i.eval(); vt <- t.eval(); ve <- e.eval()) yield if(vi) vt else ve +} + +case class Const[T](x:T) extends Expr[T]{ + def eval():Some[T] = Some(x) +} + +case class DMap[T](m:Map[TargetId,Expr[T]]) extends Expr[Map[TargetId,T]]{ + def eval(): Option[Map[TargetId, T]] = { + val values = m.transform((_,v) => v.eval()) + if(values.values.exists(_.isEmpty)) + None + else + Some(values.transform((_,v) => v.get)) + } +} + +case class Of[T](m:Variable[Map[TargetId,T]], id:TargetId) extends Expr[T]{ + def eval(): Option[T] = for {mv <- m.eval(); vv <- mv.get(id)} yield vv +} + +case class Worst[T](l:Expr[T]*)(implicit ev:IsCriticityOrdering[T], evF:IsFinite[T]) extends Expr[T] { + val ordering : IsCriticityOrdering[T] = ev + val finite : IsFinite[T] = evF + def eval(): Option[T] = { + val values = for {e <- l; ve <- e.eval()} yield ve + if(values.size != l.size) + None + else + Some(values.max) + } +} + +case class Best[T](l:Expr[T]*)(implicit evO:IsCriticityOrdering[T], evF:IsFinite[T]) extends Expr[T] { + val ordering : IsCriticityOrdering[T] = evO + val finite : IsFinite[T] = evF + def eval(): Option[T] = { + val values = for {e <- l; ve <- e.eval()} yield ve + if(values.size != l.size) + None + else + Some(values.min) + } +} + +sealed trait BoolExpr extends Expr[Boolean] + +case class Equal(l:Expr[_], r:Expr[_]) extends BoolExpr { + def eval() : Option[Boolean] = for {vl <- l.eval(); vr <- r.eval()} yield vr == vl +} + +case class And(l:BoolExpr*) extends BoolExpr { + override def eval(): Option[Boolean] = { + val and = for {e <- l; ve <- e.eval()} yield ve + if(and.size != l.size) + None + else + Some(and.forall(x => x)) + } +} + +case class Or(l:BoolExpr*) extends BoolExpr { + override def eval(): Option[Boolean] = { + val or = for {e <- l; ve <- e.eval()} yield ve + if(or.size != l.size) + None + else + Some(or.exists(x => x)) + } +} + +case class Not(n:BoolExpr) extends BoolExpr { + override def eval(): Option[Boolean] = + for {vn <- n.eval()} yield !vn +} + +sealed trait Variable[T] extends Expr[T]{ + + val id : VariableId + + def eval():Option[T] + + override def toString: String = id.name.name +} + +object Variable { + +// implicit def defOutPort[T](f: => Option[T]) : OutputPort[T] = OutputPort(() => f) +// + implicit def toOptionList[T](o:OutputPort[T]) : OutputPort[List[T]] = OutputPort(o.id, () => for (i <- o.eval()) yield List(i)) +// +// implicit def defLocalVar[T](f: => Option[T]) : LocalVariable[T] = LocalVariable(() => f) +} + +case class OutputPort[T](id : VariableId, evalFun : () => Option[T]) extends Variable[T] { + def eval(): Option[T] = for( x <- evalFun()) yield x + def |+|(that : OutputPort[T]) : List[OutputPort[T]] = this :: that :: Nil +} + +case class LocalVariable[T](id: VariableId, evalFun : () => Option[T]) extends Variable[T] { + def eval(): Option[T] = for( x <- evalFun()) yield x +} + +class InputPort[T](val id:VariableId) extends Variable[T] { + + private var evalFun : () => Option[T]= () => None + + def := [U](other:List[OutputPort[U]])(implicit linker: Linker, ev:Leibniz[Nothing,Any,List[U],T]) : Unit = { + linker.links.get(this) match { + case Some(l) => linker.links(this) = l ++ other + case None => linker.links(this) = other.toSet + } + evalFun = () => Some(ev(other.flatMap(_.evalFun()))) + } + + def := (other:OutputPort[T])(implicit linker: Linker) : Unit = { + linker.links.get(this) match { + case Some(l) => linker.links(this) = l + other + case None => linker.links(this) = Set(other) + } + evalFun = () => other.evalFun() + } + + def := (other:InputPort[T])(implicit linker: Linker) : Unit ={ + linker.links.get(this) match { + case Some(l) => linker.links(this) = l + other + case None => linker.links(this) = Set(other) + } + evalFun = () => other.evalFun() + } + + def := (f: => Option[T]) : Unit = { + evalFun = () => f + } + + def eval():Option[T] = for( x <- evalFun()) yield x +} + +object InputPort { + def apply[T](name:Symbol): InputPort[T] = new InputPort(VariableId(name)) +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/operators/IsCriticityOrdering.scala b/src/main/scala/views/dependability/operators/IsCriticityOrdering.scala new file mode 100644 index 0000000..e2bf274 --- /dev/null +++ b/src/main/scala/views/dependability/operators/IsCriticityOrdering.scala @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.operators +trait IsCriticityOrdering[T] extends Ordering[T] + +trait IsCriticityOrderingOps { + def min[T:IsCriticityOrdering : IsFinite] : T = allOf[T].min + def max[T:IsCriticityOrdering : IsFinite] : T = allOf[T].max + def worst[T:IsCriticityOrdering](l:T*) : T = l.max + def best[T:IsCriticityOrdering](l:T*) : T = l.min + + implicit class CriticityOrderOps[T: IsCriticityOrdering](a:T) extends Ordered[T]{ + def compare(that: T): Int = IsCriticityOrdering[T].compare(a,that) + } +} + +object IsCriticityOrdering{ + def apply[T](implicit ev : IsCriticityOrdering[T]) : IsCriticityOrdering[T] = ev +} diff --git a/src/main/scala/views/dependability/operators/IsFinite.scala b/src/main/scala/views/dependability/operators/IsFinite.scala new file mode 100644 index 0000000..46edffe --- /dev/null +++ b/src/main/scala/views/dependability/operators/IsFinite.scala @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.operators + +trait IsFinite[T] { + def typeName: Symbol = Symbol(all.map(fm => name(fm).name.charAt(0).toTitleCase).mkString("")) + def allWithNone:Seq[T] + def name(x:T):Symbol + val none : T + def all : Seq[T] = allWithNone.filterNot(_ == none) +} + +trait IsFiniteOps { + + implicit class hasName[T](x: T)(implicit ev: IsFinite[T]) { + def name: Symbol = ev.name(x) + } + + def nameOf[T](implicit ev: IsFinite[T]): Symbol = ev.typeName + + def allOf[T](implicit ev: IsFinite[T]): Seq[T] = ev.all + + def noneOf[T](implicit ev: IsFinite[T]): T = ev.none + + def allWithNone[T](implicit ev: IsFinite[T]): Seq[T] = ev.allWithNone +} + +object IsFinite { + implicit def tupleIsFinite[T,U] (implicit evT:IsFinite[T], evU:IsFinite[U]) : IsFinite[(T,U)] = new IsFinite[(T, U)] { + override def typeName: Symbol = Symbol(s"Pair${evT.typeName.name}${evU.typeName.name}") + def allWithNone: Seq[(T, U)] = evT.all.flatMap(t => evU.all.map(u => (t,u))) :+ none + def name(x: (T, U)): Symbol = Symbol(s"${evT.name(x._1).name}_${evU.name(x._2).name}") + val none: (T, U) = (evT.none, evU.none) + } +} diff --git a/src/main/scala/views/dependability/operators/IsMergeable.scala b/src/main/scala/views/dependability/operators/IsMergeable.scala new file mode 100644 index 0000000..b4b80af --- /dev/null +++ b/src/main/scala/views/dependability/operators/IsMergeable.scala @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.operators + +import scala.language.higherKinds + +trait IsMergeable[C[_,_]] { + def mergeWith[K,V](first:C[K,V],that:C[K,V], f:(V,V) => V) : C[K,V] +} + +trait IsMergeableOps { + implicit class IsMergeableOps[K,V,C[_,_]](a:C[K,V]){ + def mergeWith(that:C[K,V], f:(V,V) => V)(implicit ev:IsMergeable[C]) : C[K,V] = { + ev.mergeWith(a,that,f) + } + } +} + +object IsMergeable { + + def apply[C[_,_]](implicit ev:IsMergeable[C]) : IsMergeable[C] = ev + + implicit def mapIsMergeable:IsMergeable[Map] = { + new IsMergeable[Map] { + def mergeWith[K, V](first: Map[K, V], that: Map[K, V], f: (V, V) => V): Map[K, V] = { + first.foldLeft(that)((acc,kv) => + if (acc.contains(kv._1)) acc + (kv._1 -> f(acc(kv._1), kv._2)) else acc + kv + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/operators/IsOptionLike.scala b/src/main/scala/views/dependability/operators/IsOptionLike.scala new file mode 100644 index 0000000..8d21014 --- /dev/null +++ b/src/main/scala/views/dependability/operators/IsOptionLike.scala @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.operators + +trait IsOptionLike[T] { + def isDefined(x:T):Boolean = x == none + val none : T + def toOption(x:T) : Option[T] = { + if (x == none) { + None + }else { + Some(x) + } + } +} + +trait IsOptionLikeOps { + implicit class IsOptionLikeOps[T] (x:T)(implicit ev:IsOptionLike[T]) { + def isDefined: Boolean = ev.isDefined(x) + val none : T = ev.none + def toOption : Option[T] = ev.toOption(x) + } + + def none[T](implicit ev:IsOptionLike[T]) : T = ev.none +} diff --git a/src/main/scala/views/dependability/operators/IsShadowOrdering.scala b/src/main/scala/views/dependability/operators/IsShadowOrdering.scala new file mode 100644 index 0000000..f17ce0d --- /dev/null +++ b/src/main/scala/views/dependability/operators/IsShadowOrdering.scala @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.dependability.operators + +trait IsShadowOrdering[T] { + def containerShadow(init:T, containerState:T) : T + def corruptingFM(fm:T) : Boolean + def inputShadow(input:T, containerState : T) : T +} + +trait IsShadowOrderingOps { + implicit class IsShadowingOps[T : IsShadowOrdering](a:T) { + def containerShadow(containerState:T) : T = IsShadowOrdering[T].containerShadow(a,containerState) + def isCorruptingFM : Boolean = IsShadowOrdering[T].corruptingFM(a) + def inputShadow(containerState : T) : T = IsShadowOrdering[T].inputShadow(a,containerState) + } +} + +object IsShadowOrdering{ + def apply[T](implicit ev: IsShadowOrdering[T]) : IsShadowOrdering[T] = ev +} \ No newline at end of file diff --git a/src/main/scala/views/dependability/operators/package.scala b/src/main/scala/views/dependability/operators/package.scala new file mode 100644 index 0000000..cfdbda9 --- /dev/null +++ b/src/main/scala/views/dependability/operators/package.scala @@ -0,0 +1,6 @@ +package views.dependability + +package object operators extends IsFiniteOps + with IsMergeableOps + with IsShadowOrderingOps + with IsCriticityOrderingOps diff --git a/src/main/scala/views/interference/examples/package.scala b/src/main/scala/views/interference/examples/package.scala new file mode 100644 index 0000000..5c12c4f --- /dev/null +++ b/src/main/scala/views/interference/examples/package.scala @@ -0,0 +1,7 @@ +package views.interference + +/** + * Package containing PML examples of simple platforms illustrating the basic interference + * modelling and analysis features of PML + */ +package object examples diff --git a/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneApplicativeTableBasedInterferenceSpecification.scala b/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneApplicativeTableBasedInterferenceSpecification.scala new file mode 100644 index 0000000..3a43500 --- /dev/null +++ b/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneApplicativeTableBasedInterferenceSpecification.scala @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.examples.simpleKeystone + +import pml.examples.simpleKeystone.{SimpleKeystoneLibraryConfiguration, SimpleKeystonePlatform, SimpleKeystoneTransactionLibrary, SimpleSoftwareAllocation} +import pml.operators._ +import views.interference.model.specification.ApplicativeTableBasedInterferenceSpecification +import views.interference.operators._ + +/** + * The interference calculus assumptions for the SimpleKeystone's applications are gathered here. + * For instance app22 and app3 cannot execute simultaneously so + * {{{app22 exclusiveWith app3}}} + * The app3 transfer transaction does not significantly impact the [[pml.examples.simpleKeystone.SimpleKeystonePlatform.TeraNet]] + * {{{app3_transfer notInterfereWith TeraNet.periph_bus.loads}}} + * @see [[views.interference.operators.Exclusive.Ops]] for interfere operator definition + */ +trait SimpleKeystoneApplicativeTableBasedInterferenceSpecification extends ApplicativeTableBasedInterferenceSpecification { + self: SimpleKeystonePlatform with SimpleKeystoneTransactionLibrary with SimpleKeystoneLibraryConfiguration with SimpleSoftwareAllocation => + + app22 exclusiveWith app3 + + t11_app1_rd_interrupt1 notInterfereWith axi_bus.loads + app3_transfer notInterfereWith TeraNet.periph_bus.loads + app3_transfer notInterfereWith TeraNet.periph_bus.stores + app3_transfer notInterfereWith MemorySubsystem.msmc.loads +} diff --git a/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneInterferenceGeneration.scala b/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneInterferenceGeneration.scala new file mode 100644 index 0000000..dd9aafd --- /dev/null +++ b/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystoneInterferenceGeneration.scala @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.examples.simpleKeystone + +import pml.examples.simpleKeystone.SimpleKeystoneExport.* +import views.interference.operators.* + +import scala.concurrent.duration.* +import scala.language.postfixOps + +/** + * Compute the interference of the SimpleKeystone defined in [[pml.examples.simpleKeystone.SimpleKeystoneExport]] + */ +object SimpleKeystoneInterferenceGeneration extends App { + + for (p <- Set( + SimpleKeystoneConfiguredFull, + SimpleKeystoneConfiguredNoL1, + SimpleKeystoneConfiguredPlanApp21, + SimpleKeystoneConfiguredPlanApp22)) { + + // Compute only up to 2-ite and 2-free + p.computeKInterference(2, 2 hours) + + // Compute all ite and itf for benchmarks + p.computeAllInterference( 2 hours, ignoreExistingAnalysisFiles = true) + } +} diff --git a/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystonePhysicalTableBasedInterferenceSpecification.scala b/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystonePhysicalTableBasedInterferenceSpecification.scala new file mode 100644 index 0000000..12af772 --- /dev/null +++ b/src/main/scala/views/interference/examples/simpleKeystone/SimpleKeystonePhysicalTableBasedInterferenceSpecification.scala @@ -0,0 +1,51 @@ +package views.interference.examples.simpleKeystone + +import pml.examples.simpleKeystone.SimpleKeystonePlatform +import pml.operators._ +import views.interference.model.specification.PhysicalTableBasedInterferenceSpecification +import views.interference.operators._ + +/** + * The interference calculus assumptions for the hardware components of the SimpleKeystone are gathered here. + * For instance to specify that two service l and r interfere with each other if + * + * - they are provided by the same owner except for + * [[pml.examples.simpleKeystone.SimpleKeystonePlatform.TeraNet.periph_bus]], [[pml.examples.simpleKeystone.SimpleKeystonePlatform.axi_bus]], + * [[pml.examples.simpleKeystone.SimpleKeystonePlatform.MemorySubsystem.msmc]] + * - they are provided by the [[pml.examples.simpleKeystone.SimpleKeystonePlatform.dma]] and [[pml.examples.simpleKeystone.SimpleKeystonePlatform.dma_reg]] + * {{{ + * for { + * l <- services + * r <- services + * if l != r + * if (l.hardwareOwnerIs(dma) && r.hardwareOwnerIs(dma_reg)) || + * (l.hardwareOwner == r.hardwareOwner && !l.hardwareOwnerIs(TeraNet.periph_bus) + * && !l.hardwareOwnerIs(axi_bus) && !l.hardwareOwnerIs(MemorySubsystem.msmc)) + * } yield { + * l interfereWith r + * } + * }}} + */ +trait SimpleKeystonePhysicalTableBasedInterferenceSpecification extends PhysicalTableBasedInterferenceSpecification { + self: SimpleKeystonePlatform => + + for { + l <- services + r <- services + if l != r + if (l.hardwareOwnerIs(dma) && r.hardwareOwnerIs(dma_reg)) || + (l.hardwareOwner == r.hardwareOwner && !l.hardwareOwnerIs(TeraNet.periph_bus) && !l.hardwareOwnerIs(axi_bus) && !l.hardwareOwnerIs(MemorySubsystem.msmc)) + } yield { + l interfereWith r + } + + for { + l <- transactions + r <- transactions + if l != r + if l.initiator == r.initiator + } yield { + l exclusiveWith r + } + +} diff --git a/src/main/scala/views/interference/examples/simpleKeystone/package.scala b/src/main/scala/views/interference/examples/simpleKeystone/package.scala new file mode 100644 index 0000000..dc08d7e --- /dev/null +++ b/src/main/scala/views/interference/examples/simpleKeystone/package.scala @@ -0,0 +1,10 @@ +package views.interference.examples + +/** + * Package containing an example on a simplification of a TI Keystone platform + * @see [[SimpleKeystonePhysicalTableBasedInterferenceSpecification]] provides an example of hardware interference assumption modelling + * @see [[SimpleKeystoneApplicativeTableBasedInterferenceSpecification]] provides an example of application interference assumption modelling + * @see [[SimpleKeystoneInterferenceGeneration]] provides an example of interference analysis for the simplified + * Keystone platform + */ +package object simpleKeystone \ No newline at end of file diff --git a/src/main/scala/views/interference/examples/simpleT1042/SimpleT1042InterferenceGeneration.scala b/src/main/scala/views/interference/examples/simpleT1042/SimpleT1042InterferenceGeneration.scala new file mode 100644 index 0000000..5fb9056 --- /dev/null +++ b/src/main/scala/views/interference/examples/simpleT1042/SimpleT1042InterferenceGeneration.scala @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.examples.simpleT1042 + +import pml.examples.simpleT1042.SimpleT1042Export._ +import views.interference.operators._ + +import scala.concurrent.duration._ +import scala.language.postfixOps + +object SimpleT1042InterferenceGeneration extends App { + + + for (p <- Set(SimpleT1042ConfiguredFull,SimpleT1042ConfiguredNoL1,SimpleT1042ConfiguredPlanApp21,SimpleT1042ConfiguredPlanApp22)) { + + // Compute only up to 2-ite and 2-free + p.computeKInterference( 2, 2 hours) + + // Compute all ite and itf for benchmarks + p.computeAllInterference( 2 hours) + } + +} diff --git a/src/main/scala/views/interference/examples/simpleT1042/SimpleT1042TableBasedInterferenceSpecification.scala b/src/main/scala/views/interference/examples/simpleT1042/SimpleT1042TableBasedInterferenceSpecification.scala new file mode 100644 index 0000000..221da9f --- /dev/null +++ b/src/main/scala/views/interference/examples/simpleT1042/SimpleT1042TableBasedInterferenceSpecification.scala @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.examples.simpleT1042 + +import pml.examples.simpleT1042.{SimpleSoftwareAllocation, SimpleT1042LibraryConfiguration, SimpleT1042Platform, SimpleT1042TransactionLibrary} +import pml.operators._ +import views.interference.model.specification.{ApplicativeTableBasedInterferenceSpecification, PhysicalTableBasedInterferenceSpecification} +import views.interference.operators._ + +trait SimpleT1042PhysicalTableBasedInterferenceSpecification extends PhysicalTableBasedInterferenceSpecification { + self: SimpleT1042Platform => + + for { + l <- services + r <- services + if l != r + if (l.hardwareOwnerIs(dma) && r.hardwareOwnerIs(config_bus)) || (l.hardwareOwner == r.hardwareOwner && !l.hardwareOwnerIs(bus)) + } yield { + l interfereWith r + } + + for { + l <- transactions + r <- transactions + if l != r + if l.initiator == r.initiator + } yield { + l exclusiveWith r + } +} + +trait SimpleT1042ApplicativeTableBasedInterferenceSpecification extends ApplicativeTableBasedInterferenceSpecification { + self: SimpleT1042Platform with SimpleT1042TransactionLibrary with SimpleT1042LibraryConfiguration with SimpleSoftwareAllocation => + + app22 exclusiveWith app3 +} diff --git a/src/main/scala/views/interference/exporters/IDPExporter.scala b/src/main/scala/views/interference/exporters/IDPExporter.scala new file mode 100644 index 0000000..c854141 --- /dev/null +++ b/src/main/scala/views/interference/exporters/IDPExporter.scala @@ -0,0 +1,257 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.exporters + +import pml.exporters.FileManager +import pml.model.configuration.TransactionLibrary +import pml.model.hardware.{Initiator, Platform, Target, Transporter} +import pml.model.utils.Message +import pml.operators._ +import scalaz.Memo.immutableHashMapMemo +import views.interference.model.specification.InterferenceSpecification +import views.interference.model.specification.InterferenceSpecification._ + +import java.io.{FileWriter, Writer} +import scala.collection.mutable.{HashMap => MHashMap} + +object IDPExporter { + + trait Ops { + + implicit class IdpExporterOps(platform: Platform with InterferenceSpecification) { + def exportAsIDP()(implicit exporter: IDPPlatformExporter.type ): Unit = { + val startDate = System.currentTimeMillis() + val writer = new FileWriter(FileManager.exportDirectory.getFile(platform.fullName + ".pia")) + exporter.exportIDP(platform)(writer) + writer.close() + println(Message.successfulExportInfo(platform.fullName, System.currentTimeMillis() - startDate)) + } + } + + } + + implicit object IDPPlatformExporter { + + // memoization of path id + private val _memoPathId = MHashMap.empty[PhysicalTransaction,String] + + // path id is formatted as "$head_$last_$i_path" where i is the number of path with the same + // origin and destination as the one on build (possible when multiple paths in the architecture) + def pathId(p:PhysicalTransaction):String = _memoPathId.getOrElseUpdate(p,{ + val sameHT = _memoPathId.keys.count(k => k.head == p.head && k.last == p.last) + s"${p.head.name.name}_${p.last.name.name}_${sameHT}_path" + }) + + /** + * Compute all the path from a given element to the leaf services + * This methods handle cyclic graphs by simply cutting the loop when traversing it + * @param from the initial service + * @param graph the edges of the graph + * @tparam A the type of the parent nodes + * @tparam B the type of the son nodes + * @return all the possible paths + */ + def pathsIn[A, B<:A](from: A, graph: Map[A, Set[B]]): Set[Path[A]] = { + + /** + * This function value compute the path from a node of the graph to its leaf nodes (first element of the Pair). + * A set of visited nodes is also provided (second element of the Pair) to cut cycles + * The result are memoized to avoid multiple computation of the paths + */ + lazy val _paths: ((A, Set[A])) => Set[Path[A]] = immutableHashMapMemo { + s => + if(s._2.contains(s._1) ) { + println(Message.cyclicGraphWarning) + Set(Nil) + } + else if (!graph.contains(s._1) || graph(s._1).isEmpty) + Set(Nil) + else + graph(s._1).flatMap(next => _paths((next, s._2 + s._1)) map {next +: _} ) + } + + //remove empty paths (i.e. from is not connected to anyone in the graph) and add from as path head + _paths((from,Set.empty)) collect { + case p if p.nonEmpty => from +: p + } + } + + + def exportIDP(platform: Platform with InterferenceSpecification)(implicit writer: Writer): Unit = { + import platform._ + + writer.write( + s"""|/* --------------------------------------------- */ + |// PIA MODEL GENERATED FOR ${platform.name.name} + |/* --------------------------------------------- */ + | + |""".stripMargin) + + // the hw connection graph only contains physical connection used by at least one transaction + val hwGraph = platform.hardwareGraph() + val hwLinks = hwGraph flatMap { p => p._2 map { x => (p._1, x) } } + val hwComponents = hwLinks.flatMap { p => Set(p._1, p._2) }.toSet + + writer.write("/* Targets components used by at least one software */\n") + writer.write(hwComponents.collect {case t:Target => t}.map(_.name.name).toList.sorted.mkString("Targets = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Transporter components used by at least one software */\n") + writer.write(hwComponents.collect {case t:Transporter => t}.map(_.name.name).toList.sorted.mkString("Transporters= {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Smart Initiators used by at least one software */\n") + writer.write(hwComponents.collect { case s: Initiator => s.name.name }.toList.sorted.mkString("SmartInitiators = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Initiator number */\n") + writer.write(s"Initiator_Number = ${hwComponents.count { case _: Initiator => true; case _ => false }}\n\n") + + val transactions = platform.transactionsByName + + // the interference analysis do not consider copy services, so these services must be discarded + val services = transactions.values.toSet.flatten + + // utility used by interference analysis to model the absence of transaction + val nopServices = hwComponents.collect { case i: Initiator => i.name.name -> s"${i.name.name}_NOP" } + + writer.write("/* Services used by at least one software */\n") + writer.write((services.map(_.name.name) ++ nopServices.map(_._2)).toList.sorted.mkString("Services = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Type of used services */\n") + writer.write((services.map { s => s"${s.name.name} -> ${s.typeName.name.toUpperCase}" } ++ + nopServices.map(kv => s"${kv._2} -> NOP")).toList.sorted.mkString("ServiceType = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Provider of used services */\n") + writer.write((services.flatMap { s => PLProvideService.inverse(s) map { owner => s"${s.name.name} -> ${owner.name.name}" } } ++ + nopServices.map(kv => s"${kv._2} -> ${kv._1}")).toList.sorted.mkString("ProvidedBy = {\n\t", ";\n\t", "\n}\n\n")) + + val pathNames = platform match { + case l:TransactionLibrary => + val names = l.transactionUserName + transactions.keySet.map { k => k -> {if(names(k).isEmpty) Set(s"${k}_path") else names(k).map(t => s"${t}_path") }}.toMap + case _ => transactions.keySet.map { k => k -> Set(s"${k}_path") }.toMap + } + + writer.write("/* Services paths used by at least one transaction */\n") + writer.write((pathNames.values.flatten ++ + nopServices.map(kv => s"${kv._2}_${kv._2}_path")).toList.sorted.mkString("Paths = {\n\t", ";\n\t", "\n}\n\n")) + + // the successors of a nop transaction is always nop + val nopSuccessor = nopServices.map(kv => s"${kv._2}_${kv._2}_path, ${kv._2} -> ${kv._2}") + + writer.write("/* Successor relation for paths */\n") + writer.write((pathNames.transform {(k,v) => v flatMap {name => transactions(k).sliding(2) map {l => s"$name, ${l.head} -> ${l.last}"} }}.values.flatten ++ + nopSuccessor).toList.sorted.mkString("NextPathService = {\n\t", ";\n\t", "\n}\n\n")) + + val transactionNames = platform match { + case l:TransactionLibrary => + val names = l.transactionUserName + transactions.keySet.map { k => k -> {if(names(k).isEmpty) Set(k.id.name) else names(k).map(_.id.name) }}.toMap + case _ => transactions.keySet.map { k => k -> Set(k.id.name) }.toMap + } + + writer.write("/* Single transactions triggered by software */\n") + writer.write((transactionNames.values.flatten ++ + nopServices.map(kv => s"${kv._2}_${kv._2}")).toList.sorted.mkString("SingleTransactions = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Head of service path of the single transaction */\n") + writer.write((transactionNames.transform {(k,v) => v map {name => s"$name -> ${transactions(k).head.name.name}"} }.values.flatten ++ + nopServices.map {kv => s"${kv._2}_${kv._2} -> ${kv._2}"}).toList.sorted.mkString("TransactionInitiatorService = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Target service of the single transaction */\n") + writer.write((transactionNames.transform {(k,v) => v map {name => s"$name -> ${transactions(k).last.name.name}"} }.values.flatten ++ + nopServices.map {kv => s"${kv._2}_${kv._2} -> ${kv._2}"}).toList.sorted.mkString("TransactionTargetService = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write("/* Path of the single transaction */\n") + writer.write((transactions.keySet.flatMap { k => transactionNames(k) flatMap { trName => pathNames(k) map { pName => s"$trName -> $pName" }}} ++ + nopServices.map {kv => s"${kv._2}_${kv._2} -> ${kv._2}_${kv._2}_path"}).toList.sorted.mkString("TransactionPath = {\n\t", ";\n\t", "\n}\n\n")) + + writer.write( + s""" + |/* ----------------------------- */ + |// Specific to Interference View + |/* ----------------------------- */ + | + |// Transaction "t" with which topological interference are discarded + |TransparentTransactions = ${transactions + .keySet + .filter {platform.isTransparentTransaction} + .flatMap {transactionNames} + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + | + |// Transaction "t" affects a service "s" not contained in the transaction path + |// (non topologically explicable dependency) + |TransactionAffect = ${transactions + .keySet + .flatMap { k => platform.transactionInterfereWith(k).flatMap(s => transactionNames(k) map { name => s"$name,$s"})} + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + | + |// Transaction "t" does not affect a service "s" contained in the transaction path + |// (non topologically explicable tolerance) + |TransactionNotAffect = ${transactions + .keySet + .flatMap(k => platform.transactionNotInterfereWith(k).flatMap(s => transactionNames(k) map { name => s"$name,$s"})) + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + | + |// Two initiators "i" and "i'" cannot trigger transactions simultaneously + |InitiatorsAreExclusive = ${platform.initiators + .subsets(2) + .collect {case si if platform.finalInterfereWith(si.head,si.last) => si.mkString(",")} + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + | + |// Two transactions "t" and "t'" cannot be triggered simultaneously + |TransactionsAreExclusive = ${transactions + .keySet + .subsets(2) + .filter {s => platform.finalExclusive(s.head,s.last)} + .flatMap(s => transactionNames(s.head) flatMap {leftName => transactionNames(s.last) map { rightName => s"$rightName, $leftName"}}) + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + | + |// Two services "s" and "s'" provided by the same component can be used simultaneously + |Parallel = ${services + .subsets(2) + .filter {ss => ss.head.hardwareOwner.intersect(ss.last.hardwareOwner).nonEmpty && !platform.finalInterfereWith(ss.head,ss.last)} + .map {ss => ss.mkString(",")} + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + | + |// Two services "s" and "s'" are equivalent + |Equivalence = ${platform + .serviceEquivalenceClasses(services) + .filter {_.size >=2} + .flatMap {_.sliding(2)} + .map {_.mkString(",")} + .toList + .sorted + .mkString("{\n\t",";\n\t","\n}\n\n")} + """.stripMargin) + writer.flush() + writer.close() + } + } + +} diff --git a/src/main/scala/views/interference/exporters/InterferenceGraphExporter.scala b/src/main/scala/views/interference/exporters/InterferenceGraphExporter.scala new file mode 100644 index 0000000..16f1586 --- /dev/null +++ b/src/main/scala/views/interference/exporters/InterferenceGraphExporter.scala @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + */ + +package views.interference.exporters + +import pml.exporters.UMLExporter.DOTServiceOnly +import pml.exporters.{FileManager, UMLExporter} +import pml.model.hardware.Platform +import views.interference.model.specification.InterferenceSpecification +import views.interference.model.specification.InterferenceSpecification.{PhysicalScenarioId, scenarioSetId} + +import java.io.FileWriter + +object InterferenceGraphExporter { + trait Ops { + implicit class InterferenceGraphExporterOps(x: Platform with InterferenceSpecification) extends UMLExporter.Ops { + def exportGraph(it: Set[PhysicalScenarioId]): Unit = { + val scenarioSetName = scenarioSetId(it.map(x => PhysicalScenarioId(x.id))) + implicit val writer: FileWriter = new FileWriter(FileManager.exportDirectory.getFile( + s"${x.fullName}_${if(scenarioSetName.id.name.length >= 100) scenarioSetName.hashCode.toString else scenarioSetName}.dot" + )) + DOTServiceOnly.resetService() + DOTServiceOnly.writeHeader + val services = it + .flatMap(x.purifiedScenarios) + .flatMap(x.purifiedTransactions) + + for {s <- services.subsets(2) if x.finalInterfereWith(s.head, s.last)} + DOTServiceOnly.writeAssociation(DOTServiceOnly.getId(s.head).get, DOTServiceOnly.getId(s.last).get, "exclusive") + + for {s <- it + t <- x.purifiedScenarios(s) + l = x.purifiedTransactions(t).sliding(2).collect { case Seq(f, t) => f -> t }.toList + if l.nonEmpty} { + val transaction = s"""${t.id.name}[label = "{${t.id.name} : Transaction}"]""" + writer.write(s"$transaction\n") + DOTServiceOnly.writeAssociation(t.id.name, DOTServiceOnly.getId(l.head._1).get) + l.foreach(p => DOTServiceOnly.exportUML(p._1, p._2)) + } + DOTServiceOnly.writeFooter + writer.close() + } + } + } +} diff --git a/src/main/scala/views/interference/exporters/package.scala b/src/main/scala/views/interference/exporters/package.scala new file mode 100644 index 0000000..85b0671 --- /dev/null +++ b/src/main/scala/views/interference/exporters/package.scala @@ -0,0 +1,13 @@ +package views.interference + +//FIXME The usage of exporters is not illustrated in examples +/** + * Package containing the interference related exporters + * {{{ + * scala> import views.interference.exporters._ + * }}} + * The available extension methods are provided in [[IDPExporter.Ops]] and [[InterferenceGraphExporter.Ops]] + * Example of usages are provided in ??? + */ +package object exporters extends IDPExporter.Ops + with InterferenceGraphExporter.Ops diff --git a/src/main/scala/views/interference/model/formalisation/BDDFactory.scala b/src/main/scala/views/interference/model/formalisation/BDDFactory.scala new file mode 100644 index 0000000..c787c06 --- /dev/null +++ b/src/main/scala/views/interference/model/formalisation/BDDFactory.scala @@ -0,0 +1,600 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.model.formalisation + +import net.sf.javabdd.{BDD => JavaBDD, BDDFactory => JavaBDDFactory} + +import scala.collection.mutable + +/** + * BDDFactory where variables labelled on BDD node are InstBoolIdent + */ +class SymbolBDDFactory extends GenBDDFactory[Symbol] { + protected val _factory: JavaBDDFactory = initFactory(500, 500) + protected val varMap: mutable.HashMap[Symbol, Int] = mutable.HashMap.empty + protected var _varCount = 0 + protected val nbOfVar: Int = 500 +} + +/** + * base trait for BDD factories + * @tparam Var the type of variable labelled on BDD nodes + * @tparam MyBDD the BDD representation which must be a subtype of JavaBDD type + */ +trait BaseBDDFactory[Var, MyBDD <: JavaBDD] { + + /** + * initialisation of the cache and number of node of the factory + * @param numberOfVar maximum number of variables in BDDs + * @param cacheSize initial size of the cache table containing BDD nodes + * @return + */ + def initFactory(numberOfVar: Int, cacheSize: Int): JavaBDDFactory + + /** + * Produce a BDD node labelled with a given variable + * @param variable the variable + * @return a BDD node + */ + def getVar(variable: Var): MyBDD + + /** + * Return the ith BDD node in the table + * @param i the index of the BDD node + * @return BDD node + */ + def getIthVar(i: Int): MyBDD + + /** + * Clean the cache and the index table + */ + def reset(): Unit + + /** + * Return the zero terminal + * @return zero terminal + */ + def zero(): MyBDD + + /** + * Return the one terminal + * @return one terminal + */ + def one(): MyBDD + + /** + * BDD AND + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def andBDD(left: MyBDD, right: MyBDD): MyBDD + + /** + * n-ary BDD and + * @param s set of BDD + * @return the resulting BDD + */ + def andBDD(s:Iterable[MyBDD]): MyBDD = + s.foldLeft(one())((acc,l) => andBDD(acc,l)) + + /** + * BDD OR + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def orBDD(left: MyBDD, right: MyBDD): MyBDD + + /** + * n-ary BDD or + * @param s the set of BDD + * @return the resulting BDD + */ + def orBDD(s:Iterable[MyBDD]): MyBDD = + s.foldLeft(zero())((acc,l) => orBDD(acc,l)) + + /** + * BDD negation + * @param arg initial BDD + * @return negated BDD + */ + def notBDD(arg: MyBDD): MyBDD + + /** + * BDD implication + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def mkImplies(left:MyBDD, right:MyBDD): MyBDD = + orBDD(notBDD(left),right) + + /** + * Exactly k elements out of an ordered sequence of variables + * @param vs the ordered sequence of variables + * @param k the number of variables that must be true + * @return the resulting BDD + */ + def mkExactlyK(vs:Seq[Var], k:Int): MyBDD = { + val ds = vs.distinct + val oneIdx = (-1, 0) + val zeroIdx = (0, -1) + + val _memo = mutable.HashMap.empty[(Int,Int), MyBDD] + + def BDDFromTable(of: (Int, Int), in: Map[(Int, Int), (Var, (Int, Int), (Int, Int))]): MyBDD = _memo.getOrElseUpdate(of, + if (of == oneIdx) + one() + else if (of == zeroIdx) + zero() + else { + val (v, h, l) = in(of) + getVar(v) + mkNode(v, BDDFromTable(h, in), BDDFromTable(l, in)) + }) + + if (k == 0 || ds.size < k) + zero() + else { + val linkMap = (0 to k).flatMap(nbTrue => + (0 to ds.size - k) + .filter( _ + nbTrue < ds.size) + .map(nbFalse => + (nbTrue, nbFalse) -> ( + ds(nbTrue + nbFalse), + if (nbTrue == k - 1 && nbFalse == ds.size - k) oneIdx else if (nbTrue >= k) zeroIdx else (nbTrue + 1, nbFalse), + if (nbTrue == k && nbFalse == ds.size - k - 1) oneIdx else if (nbFalse >= ds.size - k) zeroIdx else (nbTrue, nbFalse + 1) + ) + )).toMap + BDDFromTable((0, 0), linkMap) + } + } + + + /** + * Replace all BDD nodes labelled by a given to variable to another one + * @param replace the initial variable to replace + * @param by the new variable + * @param in the BDD + * @return the modified BDD + */ + def replaceVar(replace: Var, by: Var, in: MyBDD): MyBDD + + /** + * Build a MDDNode labelled by a variable where the high and low sons are given + * @param variable the variable labelling the BDD + * @param high the high son + * @param low the low son + * @return the resulting BDD + */ + def mkNode(variable: Var, high: MyBDD, low: MyBDD): MyBDD + + /** + * Free native data structure if exists + */ + def dispose(): Unit + + /** + * @return the mapping from BDD to labelled variable + */ + def getVarMap: Map[MyBDD, Var] + + /** + * Import a BDD in this factory from one coming from another factory + * @param bdd the other factory BDD + * @param bddVar the map from BDD node to variables + * @tparam OtherBDD the type of the other BDD + * @return the BDD imported in this factory + */ + def importBDD[OtherBDD <: JavaBDD](bdd: OtherBDD, bddVar: Map[Var, OtherBDD]): MyBDD = { + //add fresh vars in this factory + bddVar.foreach(kv => kv._1 -> getVar(kv._1).`var`()) + //copy node by node the initial bdd by replacing old variables by freshly created one + importRec(bdd, bddVar.map(p => p._2.`var`() -> p._1)) + } + + /** + * Build a BDD in this factory from one coming from another factory + * @param bdd the other factory BDD + * @tparam OtherBDD the type of the other BDD + * @return the BDD imported in this factory + */ + private def importRec[OtherBDD <: JavaBDD](bdd: OtherBDD, bddMap: Map[Int, Var]): MyBDD = { + if (bdd.isOne) + one() + else if (bdd.isZero) + zero() + else + mkNode(bddMap(bdd.`var`()), importRec(bdd.high(), bddMap), importRec(bdd.low(), bddMap)) + } + +} + +/** + * Class for a classic BDD Factory (variable are integer and BDD are JavaBDD) + */ +class BDDFactory extends BaseBDDFactory[Int, JavaBDD] { + + //the number of variable produced by the factory + private var _varCount = 0 + + //initial maximum number of variables + private val nbOfVar: Int = 500 + + //the JavaBDD factory used to delegate BDD computation + private val _factory: JavaBDDFactory = initFactory(nbOfVar, 1000) + + /** + * Initialise a JavaBDD factory + * @param numberOfVar maximum number of variables in BDDs + * @param cacheSize initial size of the cache table containing BDD nodes + * @return JavaBDD factory + */ + def initFactory(numberOfVar: Int, cacheSize: Int): JavaBDDFactory = { + var numberOfNodes = numberOfVar * numberOfVar + numberOfNodes = Math.max(cacheSize, numberOfNodes) + val fac = JavaBDDFactory.init("java", numberOfNodes, cacheSize) + if (fac.varNum() < numberOfVar) + fac.setVarNum(numberOfVar) + fac.autoReorder(JavaBDDFactory.REORDER_WIN2) + fac + } + + /** + * Build a variable labelled by the current number of variable produced by the factory + * + * @return BDD + */ + def produceVar(): JavaBDD = { + _varCount = _varCount + 1 + if ((_varCount % nbOfVar) == 0) + _factory.setVarNum(_varCount + nbOfVar) + + _factory.ithVar(_varCount) + } + + /** + * Increment the number of produced variable and return the corresponding BDD + * @param variable the variable + * @return a BDD node + */ + def getVar(variable: Int): JavaBDD = { + if (variable > _varCount) + _varCount = variable + _factory.ithVar(variable) + } + + /** + * Return the ith var of the JavaBDD factory + * @param i the index of the BDD node + * @return BDD node + */ + def getIthVar(i: Int): JavaBDD = { + _factory.ithVar(i) + } + + /** + * Reinitialise the JavaBDD factory to initial variable number + */ + def reset(): Unit = { + _varCount = 0 + _factory.reset() + _factory.setVarNum(nbOfVar) + + } + + /** + * @return zero terminal + */ + def zero(): JavaBDD = { + _factory.zero() + } + + /** + * + * @return one terminal + */ + def one(): JavaBDD = { + _factory.one() + } + + /** + * Delegate BDD AND to JavaBDD factory + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def andBDD(left: JavaBDD, right: JavaBDD): JavaBDD = { + left.and(right) + } + + /** + * Delegate BDD OR to JavaBDD factory + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def orBDD(left: JavaBDD, right: JavaBDD): JavaBDD = { + left.or(right) + } + + /** + * Delegate BDD NOT to JavaBDD + * @param arg initial BDD + * @return negated BDD + */ + def notBDD(arg: JavaBDD): JavaBDD = { + arg.id().not() + } + + /** + * Use JavaBDD replace to build new BDD where a given variable is replaced by another one + * @param replace the initial variable to replace + * @param by the new variable + * @param in the BDD + * @return the modified BDD + */ + def replaceVar(replace: Int, by: Int, in: JavaBDD): JavaBDD = { + val pair = _factory.makePair(replace, by) + in.replace(pair) + } + + /** + * Build a BDD bu applying the formula v.high + (neg v).low + * @param variable the variable labelling the BDD + * @param high the high son + * @param low the low son + * @return the resulting BDD + */ + def mkNode(variable: Int, high: JavaBDD, low: JavaBDD): JavaBDD = { + getIthVar(variable).and(high).or(getIthVar(variable).not().and(low)) + } + + /** + * Send dispose signal to JavaBDD factory + */ + def dispose(): Unit = { + _factory.done() + } + + /** + * Build the map from BDD identifier to BDD + * @return the mapping from BDD to labelled variable + */ + def getVarMap: Map[JavaBDD, Int] = { + (0 to _varCount).map(i => getIthVar(i) -> i).toMap + } +} + +/** + * Generic BDD factory over the type of BDD variables + * @tparam Var the type of variable labelled on BDD nodes + */ +trait GenBDDFactory[Var] extends BaseBDDFactory[Var, JavaBDD] { + + object FactoryImplicits { + import scala.language.implicitConversions + /** + * Transform variable to their BDD + * @param variable the variable labelled on the root node + * @return the BDD "< v, 1, 0>" + */ + implicit def toJavaBDD(variable: Var): JavaBDD = getVar(variable) + } + + protected var _varCount: Int + protected val _factory: JavaBDDFactory + protected val nbOfVar: Int + protected val varMap: mutable.HashMap[Var, Int] + + + /** + * Initialise a JavaBDD factory + * @param numberOfVar maximum number of variables in BDDs + * @param cacheSize initial size of the cache table containing BDD nodes + * @return + */ + def initFactory(numberOfVar: Int, cacheSize: Int): JavaBDDFactory = { + var numberOfNodes = numberOfVar * numberOfVar + numberOfNodes = Math.max(cacheSize, numberOfNodes) + val fac = JavaBDDFactory.init("java", numberOfNodes, cacheSize) + if (fac.varNum() < numberOfVar) + fac.setVarNum(numberOfVar) + fac.autoReorder(JavaBDDFactory.REORDER_WIN2) + fac + } + + /** + * Try to find the BDD of the variable in the correspondence table + * and return it if existing, otherwise generate a new BDD + * @param variable the variable + * @return a BDD node + */ + def getVar(variable: Var): JavaBDD = { + varMap.get(variable) match { + case Some(i) => + getIthVar(i) + case None => + _varCount = _varCount + 1 + + if ((_varCount % nbOfVar) == 0) + _factory.setVarNum(_varCount + nbOfVar) + val bdd = _factory.ithVar(_varCount) + varMap += (variable -> bdd.`var`() ) + bdd + } + } + + /** + * Reinitialise the JavaBDD factory to initial variable number + */ + def reset(): Unit = { + varMap.clear() + _factory.reset() + _varCount = 0 + _factory.setVarNum(nbOfVar) + } + + /** + * + * @return zero terminal + */ + def zero(): JavaBDD = { + _factory.zero() + } + + /** + * + * @return one terminal + */ + def one(): JavaBDD = { + _factory.one() + } + + /** + * Return the BDD labelled by the ith variable + * @param i the index of the BDD node + * @return BDD node + */ + def getIthVar(i: Int): JavaBDD = { + _factory.ithVar(i) + } + + /** + * Delegate BDD AND to JavaBDD factory + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def andBDD(left: JavaBDD, right: JavaBDD): JavaBDD = { + left.and(right) + } + + /** + * Delegate BDD OR to JavaBDD factory + * @param left BDD + * @param right BDD + * @return the resulting BDD + */ + def orBDD(left: JavaBDD, right: JavaBDD): JavaBDD = { + left.or(right) + } + + /** + * Delegate BDD Not to JavaBDD factory + * @param arg initial BDD + * @return negated BDD + */ + def notBDD(arg: JavaBDD): JavaBDD = { + arg.id().not() + } + + /** + * + * @param replace the initial variable to replace + * @param by the new variable + * @param in the BDD + * @return the modified BDD + */ + def replaceVar(replace: Var, by: Var, in: JavaBDD): JavaBDD = { + { + for (i1 <- varMap.get(replace); + i2 <- varMap.get(by)) + yield { + in.replace(_factory.makePair(i1, i2)) + } + }.getOrElse(throw new Exception(s"unknown variable $replace or $by")) + } + + /** + * Build a BDD bu applying the formula v.high + (neg v).low + * @param variable the variable labelling the BDD + * @param high the high son + * @param low the low son + * @return the resulting BDD + */ + def mkNode(variable: Var, high: JavaBDD, low: JavaBDD): JavaBDD = { + varMap.get(variable) match { + case Some(index) => + getIthVar(index).and(high).or(getIthVar(index).not().and(low)) + case None => + throw new Exception(s"unknown variable $variable") + } + + } + + def getPathCount(bdd:JavaBDD, weights:Map[Var,Int] = Map.empty): BigInt = { + if(weights.isEmpty) + BigInt(bdd.pathCount().toLong) + else { + val _memo = mutable.HashMap.empty[JavaBDD, BigInt] + + def memoRedSatCount(x: JavaBDD): BigInt = _memo.getOrElseUpdate(x, + if (x.isOne) + 1 + else if (x.isZero) + 0 + else + (for {v <- getVarOf(x) + w <- weights.get(v)} yield + memoRedSatCount(x.low()) + w * memoRedSatCount(x.high())).getOrElse (memoRedSatCount(x.low()) + memoRedSatCount(x.high())) + ) + + memoRedSatCount(bdd) + } + } + + def getSatCount(bdd: JavaBDD, weights: Map[Var, Int] = Map.empty): BigInt = { + val _memo = mutable.HashMap.empty[(JavaBDD,Int), BigInt] + + def memoRedSatCount(x: JavaBDD, remainingDecisions:Int): BigInt = _memo.getOrElseUpdate((x,remainingDecisions), + if (x.isOne) + BigInt(2).pow(remainingDecisions) + else if (x.isZero) + 0 + else + (for {v <- getVarOf(x) + w <- weights.get(v)} yield + memoRedSatCount(x.low(), remainingDecisions - 1) + w * memoRedSatCount(x.high(), remainingDecisions - 1)).getOrElse (memoRedSatCount(x.low(), remainingDecisions - 1) + memoRedSatCount(x.high(), remainingDecisions - 1)) + ) + + memoRedSatCount(bdd, varMap.size) + } + + /** + * Send dispose signal to JavaBDD factory + */ + def dispose(): Unit = { + _factory.done() + varMap.clear() + _varCount = 0 + } + + /** + * + * @return the mapping from BDD to labelled variable + */ + def getVarMap: Map[JavaBDD, Var] = { + varMap.map(kv => getIthVar(kv._2) -> kv._1).toMap + } + + def getVarOf(bdd: JavaBDD): Option[Var] = + for {r <- varMap.find(_._2 == bdd.`var`())} + yield r._1 +} diff --git a/src/main/scala/views/interference/model/formalisation/ProblemElement.scala b/src/main/scala/views/interference/model/formalisation/ProblemElement.scala new file mode 100644 index 0000000..e7e8518 --- /dev/null +++ b/src/main/scala/views/interference/model/formalisation/ProblemElement.scala @@ -0,0 +1,237 @@ +package views.interference.model.formalisation + +import monosat.Logic._ +import monosat.{Comparison, Graph, Lit, Solver} +import pml.model.configuration.TransactionLibrary +import pml.model.configuration.TransactionLibrary.UserScenarioId +import pml.model.hardware.Platform +import pml.model.service.Service +import scalaz.Memo.immutableHashMapMemo +import views.interference.model.formalisation.ProblemElement._ +import views.interference.model.specification.InterferenceSpecification.{Channel, PhysicalScenario, PhysicalScenarioId} +import views.interference.model.specification.{ApplicativeTableBasedInterferenceSpecification, InterferenceSpecification} + +import scala.jdk.CollectionConverters._ + +trait ProblemElement + +object ProblemElement { + type NodeId = Symbol + type EdgeId = Symbol + type LitId = Symbol + +} + +trait ALit extends ProblemElement { + val toLit: Solver => Lit +} + +trait Assert extends ProblemElement { + def assert(s: Solver): Unit +} + +case class SimpleAssert(l: ALit) extends Assert { + def assert(s: Solver): Unit = s.assertTrue(l.toLit(s)) +} + +case class AssertPB(l: Set[ALit], comp: Comparison, k: Int) extends Assert { + def assert(s: Solver): Unit = s.assertPB(l.map(_.toLit(s)).toSeq.asJava, comp, k) +} + +case class MNode(id: NodeId) extends ProblemElement + +case class MGraph(nodes: Set[MNode], edges: Set[MEdge]) extends ProblemElement { + val toGraph: Solver => Graph = immutableHashMapMemo { + s => { + val g = new Graph(s) + + val addNode: MNode => Int = immutableHashMapMemo { + s => g.addNode(s.id.name) + } + + val addEdge: MEdge => Lit = immutableHashMapMemo { + e => g.addUndirectedEdge(addNode(e.from), addNode(e.to), e.id.name) + } + + nodes.foreach(addNode) + edges.foreach(addEdge) + g + } + } +} + +case class MEdge(from: MNode, to: MNode, id: EdgeId) extends ProblemElement + +case class MEdgeLit(edge: MEdge, graph: MGraph) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => graph.toGraph(s).getEdge(edge.id.name).l } +} + +case class MLit(id: LitId) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => new Lit(s, id.name) } +} + +case class And(l: ALit*) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => and(l.map(_.toLit(s)).asJava) } +} + +case class Or(l: ALit*) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => or(l.map(_.toLit(s)).asJava) } +} + +case class Implies(l: ALit, r: ALit) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => implies(l.toLit(s), r.toLit(s)) } +} + +case class Not(l: ALit) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => not(l.toLit(s)) } +} + +case class Equal(l: ALit, r: ALit) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => equal(l.toLit(s), r.toLit(s)) } +} + +case class Reaches(graph: MGraph, from: MNode, to: MNode) extends ALit { + val toLit: Solver => Lit = immutableHashMapMemo { s => { + val g = graph.toGraph(s) + g.reaches(g.getNode(from.id.name), g.getNode(to.id.name)) + } + } +} + +class Problem(val platform: Platform with InterferenceSpecification, + val groupedScenarios: Map[MLit, Set[PhysicalScenarioId]], + val litToNodeSet: Map[MLit, Set[Set[MNode]]], + val idToScenario: Map[PhysicalScenarioId, PhysicalScenario], + val exclusiveScenarios: Map[PhysicalScenarioId, Set[PhysicalScenarioId]], + val serviceGraph: MGraph, + val isFree: ALit, + val isITF: ALit, + val pbCst: Set[AssertPB], + val simpleCst: Set[SimpleAssert], + val nodeToServices: Map[MNode, Set[Service]], + val serviceToScenarioLit: Map[Service, Set[PhysicalScenarioId]], + val maxSize: Option[Int] + ) extends ProblemElement { + + private val nodeToScenario = nodeToServices.transform((_, v) => v.flatMap(serviceToScenarioLit)) + + def decodeUserModel(physicalModel: Set[PhysicalScenarioId]): Set[Set[UserScenarioId]] = platform match { + case _ if physicalModel.isEmpty => Set.empty + case _ if maxSize.isDefined && physicalModel.size > maxSize.get => Set.empty + case spec: TransactionLibrary => + val scenario = idToScenario + .view + .filterKeys(physicalModel) + .toMap + + val userNames = scenario + .view + .mapValues(spec.scenarioUserName) + .toMap + .transform((k, v) => if (v.isEmpty) Set(UserScenarioId(k.id)) else v) + + val results = userNames.values.tail + .foldLeft(userNames.values.head.map(n => Set(n)))((acc, names) => + for { + p <- acc + last <- platform match { + case app: ApplicativeTableBasedInterferenceSpecification => + val x = names.filter(id => + app.finalUserScenarioExclusive(id).intersect(p).isEmpty) + x + case _ => names + } + } yield { + p + last + }) + results + case _ => Set(physicalModel.map(s => UserScenarioId(s.id))) + } + + //FIXME Are we integrating exclusive service in the channel even if not used in the scenario? + def decodeChannel(model: Set[PhysicalScenarioId]): Channel = { + if (maxSize.isDefined && model.size > maxSize.get) + Set.empty + else + nodeToScenario + .keySet + .filter(k => model.intersect(nodeToScenario(k)).size >= 2) + .flatMap(nodeToServices) + } + + def decodeModel(model: Set[MLit], isFree: Boolean): Set[Set[PhysicalScenarioId]] = { + if (maxSize.isDefined && model.size > maxSize.get) + Set.empty + else if (model.size == 1 && groupedScenarios(model.head).size == 1) { + Set.empty + } else if (model.forall(v => groupedScenarios(v).size == 1)) { + Set(model map { v => groupedScenarios(v).head }) + } else { + val s = Problem.getNewSolver("-decide-theories") + val scenarios = model.flatMap(groupedScenarios) + val variables = scenarios + .map(k => k -> new Lit(s, k.id.name)) + .toMap + variables.foreach(kv => s.assertTrue(implies(kv._2, and(exclusiveScenarios(kv._1).intersect(variables.keySet).map(variables).map(not).asJava)))) + s.assertAnd(model.map(groupedScenarios).map(st => or(st.map(variables).asJava)).asJava) + for {m <- maxSize} yield s.assertPB(variables.values.toSeq.asJava, LEQ, m) + if (model.size == 1) + s.assertPB(variables.values.toSeq.asJava, GEQ, 2) + if (isFree) + model + .filter(m => litToNodeSet(m).exists(_.nonEmpty)) + .foreach(l => s.assertAtMostOne(groupedScenarios(l).map(variables).asJava)) + val decodedModels = collection.mutable.Set.empty[Set[PhysicalScenarioId]] + while (s.solve()) { + val (positiveModel, negativeModel) = variables.keySet.partition(k => variables(k).value()) + decodedModels += positiveModel + s.assertOr((positiveModel.map(variables).map(not) ++ negativeModel.map(variables)).asJava) + } + s.close() + decodedModels.toSet + } + } + + def instantiate(k: Int): Solver = { + val s = Problem.getNewSolver("-decide-theories") + serviceGraph.toGraph(s) + s.assertTrue(or(isITF.toLit(s), isFree.toLit(s))) + pbCst.foreach(_.assert(s)) + simpleCst.foreach(_.assert(s)) + s.assertPB(groupedScenarios.keySet.map(_.toLit(s)).toSeq.asJava, EQ, k) + s + } +} + +object Problem { + def apply(platform: Platform with InterferenceSpecification, + groupedScenarios: Map[MLit, Set[PhysicalScenarioId]], + litToNodeSet: Map[MLit, Set[Set[MNode]]], + idToScenario: Map[PhysicalScenarioId, PhysicalScenario], + exclusiveScenarios: Map[PhysicalScenarioId, Set[PhysicalScenarioId]], + serviceGraph: MGraph, + isFree: ALit, + isITF: ALit, + pbCst: Set[AssertPB], + simpleCst: Set[SimpleAssert], + nodeToServices: Map[MNode, Set[Service]], + serviceToScenarioLit: Map[Service, Set[PhysicalScenarioId]], + maxSize: Option[Int] = None + ): Problem = + new Problem( + platform, + groupedScenarios, + litToNodeSet, + idToScenario, + exclusiveScenarios, + serviceGraph, + isFree, + isITF, + pbCst, + simpleCst, + nodeToServices, + serviceToScenarioLit, + maxSize) + + private def getNewSolver(s:String) : Solver = new Solver(s) +} \ No newline at end of file diff --git a/src/main/scala/views/interference/model/formalisation/package.scala b/src/main/scala/views/interference/model/formalisation/package.scala new file mode 100644 index 0000000..9f656a7 --- /dev/null +++ b/src/main/scala/views/interference/model/formalisation/package.scala @@ -0,0 +1,8 @@ +package views.interference.model + +/** + * Package containing model extension enabling formalisation of the interference computation problem as an SMT problem. + * @see [[BDDFactory]] provides some basic Binary Decision Diagram features based on JavaBDD. + * @see [[ProblemElement]] provides all the necessary modelling features to encode the problem with Monosat + */ +package object formalisation diff --git a/src/main/scala/views/interference/model/package.scala b/src/main/scala/views/interference/model/package.scala new file mode 100644 index 0000000..842237e --- /dev/null +++ b/src/main/scala/views/interference/model/package.scala @@ -0,0 +1,10 @@ +package views.interference + +/** + * Package containing the model extensions for interference analysis + * + * @see [[views.interference.model.specification]] for modelling features for interference analysis specification + * @see [[views.interference.model.formalisation]] for internal models used to formalise the problem as SMT problem + * @note internal models should not be used for platform analysis or modelling + */ +package object model diff --git a/src/main/scala/views/interference/model/relations/EquivalenceRelation.scala b/src/main/scala/views/interference/model/relations/EquivalenceRelation.scala new file mode 100644 index 0000000..60e9f38 --- /dev/null +++ b/src/main/scala/views/interference/model/relations/EquivalenceRelation.scala @@ -0,0 +1,18 @@ +package views.interference.model.relations + +import pml.model.relations.{ReflexiveSymmetricEndomorphism, Endomorphism} +import pml.model.service.Service +import pml.model.utils.Message +import sourcecode.Name + +case class EquivalenceRelation[A] private(iniValues: Map[A, Set[A]])(using n:Name) extends ReflexiveSymmetricEndomorphism[A](iniValues) + +object EquivalenceRelation{ + trait Instances { + /** + * Relation gathering user defined equivalent services + * @group equivalence_relation + */ + final implicit val serviceEquivalent: EquivalenceRelation[Service] = EquivalenceRelation(Map.empty) + } +} \ No newline at end of file diff --git a/src/main/scala/views/interference/model/relations/ExclusiveRelation.scala b/src/main/scala/views/interference/model/relations/ExclusiveRelation.scala new file mode 100644 index 0000000..38f874e --- /dev/null +++ b/src/main/scala/views/interference/model/relations/ExclusiveRelation.scala @@ -0,0 +1,33 @@ +package views.interference.model.relations + +import pml.model.configuration.TransactionLibrary.UserScenarioId +import pml.model.relations.{AntiReflexiveSymmetricEndomorphism, Endomorphism} +import pml.model.software.Application +import sourcecode.Name +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +case class ExclusiveRelation[A] private(iniValues: Map[A, Set[A]])(using n:Name) extends AntiReflexiveSymmetricEndomorphism[A](iniValues) + +object ExclusiveRelation{ + trait GeneralInstances { + /** + * Relation gathering user defined exclusive transactions + * @group exclusive_relation + */ + final implicit val transactionExclusive: ExclusiveRelation[PhysicalTransactionId] = ExclusiveRelation(Map.empty) + } + trait LibraryInstances { + /** + * Relation gathering user defined exclusive scenarios + * @group exclusive_relation + */ + final implicit val userScenarioExclusive: ExclusiveRelation[UserScenarioId]= ExclusiveRelation(Map.empty) + } + trait ApplicationInstances { + /** + * Relation gather user defined exclusive software + * @group exclusive_relation + */ + final implicit val swExclusive: ExclusiveRelation[Application] = ExclusiveRelation(Map.empty) + } +} \ No newline at end of file diff --git a/src/main/scala/views/interference/model/relations/InterfereRelation.scala b/src/main/scala/views/interference/model/relations/InterfereRelation.scala new file mode 100644 index 0000000..6038996 --- /dev/null +++ b/src/main/scala/views/interference/model/relations/InterfereRelation.scala @@ -0,0 +1,32 @@ +package views.interference.model.relations + +import pml.model.hardware.Hardware +import pml.model.relations.Relation +import pml.model.service.Service +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +case class InterfereRelation[L,R] private(iniValues: Map[L, Set[R]]) extends Relation[L,R](iniValues) + +//FIXME TO ENSURE CORRECTNESS THE INTERFERE ENDOMORPHISMS SHOULD BE ANTI-REFLEXIVE AND SYMMETRIC TO BE +// CONSISTENT WITH INTERFERENCE SPECIFICATION BASE TRAIT +object InterfereRelation{ + trait Instances { + /** + * Relation gathering user defined service interferences caused by a transaction + * @group interfere_relation + */ + final implicit val physicalTransactionIdInterfereWithService: InterfereRelation[PhysicalTransactionId, Service] = InterfereRelation(Map.empty) + + /** + * Relation gathering user defined service interferences + * @group interfere_relation + */ + final implicit val serviceInterfereWithService: InterfereRelation[Service, Service] = InterfereRelation(Map.empty) + + /** + * Relation gathering user defined interfering hardware + * @group interfere_relation + */ + final implicit val hardwareExclusive: InterfereRelation[Hardware, Hardware] = InterfereRelation(Map.empty) + } +} \ No newline at end of file diff --git a/src/main/scala/views/interference/model/relations/NotInterfereRelation.scala b/src/main/scala/views/interference/model/relations/NotInterfereRelation.scala new file mode 100644 index 0000000..a9da4d0 --- /dev/null +++ b/src/main/scala/views/interference/model/relations/NotInterfereRelation.scala @@ -0,0 +1,31 @@ +package views.interference.model.relations + +import pml.model.hardware.Hardware +import pml.model.relations.Relation +import pml.model.service.Service +import sourcecode.Name +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +case class NotInterfereRelation[L,R] private(iniValues: Map[L, Set[R]])(using n:Name) extends Relation[L,R](iniValues) + +object NotInterfereRelation{ + trait Instances { + /** + * Relation gathering user defined service non-interference caused by a transaction + * @group interfere_relation + */ + final implicit val physicalTransactionIdNotInterfereWithService: NotInterfereRelation[PhysicalTransactionId,Service] = NotInterfereRelation(Map.empty) + + /** + * Relation gathering user defined service non-interferences + * @group interfere_relation + */ + final implicit val serviceNotInterfereWithService: NotInterfereRelation[Service, Service] = NotInterfereRelation(Map.empty) + + /** + * Relation gathering user defined non-interfering hardware + * @group interfere_relation + */ + final implicit val hardwareNotExclusive: NotInterfereRelation[Hardware, Hardware] = NotInterfereRelation(Map.empty) + } +} \ No newline at end of file diff --git a/src/main/scala/views/interference/model/relations/TransparentSet.scala b/src/main/scala/views/interference/model/relations/TransparentSet.scala new file mode 100644 index 0000000..f85a412 --- /dev/null +++ b/src/main/scala/views/interference/model/relations/TransparentSet.scala @@ -0,0 +1,20 @@ +package views.interference.model.relations + +import sourcecode.Name +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +import scala.collection.mutable.Set as MSet + +case class TransparentSet[T]private(value:MSet[T])(using n:Name){ + val name:String = n.value +} + +object TransparentSet{ + trait Instances { + /** + * Set gathering discarded transactions + * @group transparent_relation + */ + final implicit val transactionIsTransparent: TransparentSet[PhysicalTransactionId] = TransparentSet(MSet.empty) + } +} diff --git a/src/main/scala/views/interference/model/relations/package.scala b/src/main/scala/views/interference/model/relations/package.scala new file mode 100644 index 0000000..8a4681b --- /dev/null +++ b/src/main/scala/views/interference/model/relations/package.scala @@ -0,0 +1,3 @@ +package views.interference.model + +package object relations diff --git a/src/main/scala/views/interference/model/specification/ApplicativeTableBasedInterferenceSpecification.scala b/src/main/scala/views/interference/model/specification/ApplicativeTableBasedInterferenceSpecification.scala new file mode 100644 index 0000000..916941b --- /dev/null +++ b/src/main/scala/views/interference/model/specification/ApplicativeTableBasedInterferenceSpecification.scala @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.interference.model.specification + +import pml.model.configuration.TransactionLibrary +import pml.model.configuration.TransactionLibrary.UserScenarioId +import pml.model.hardware.Platform +import views.interference.model.relations.ExclusiveRelation +import views.interference.operators.Transform + +trait ApplicativeTableBasedInterferenceSpecification extends TableBasedInterferenceSpecification + with Transform.TransactionLibraryInstances + with ExclusiveRelation.LibraryInstances + { + self: Platform & TransactionLibrary => + + /** + * Relation encoding the exclusivity constraints over [[pml.model.configuration.TransactionLibrary.UserScenarioId]] + * considered by the user + * @group exclusive_relation + */ + final lazy val finalUserScenarioExclusive: Map[UserScenarioId, Set[UserScenarioId]] = { + val exclusive = finalExclusive(purifiedScenarios.keySet) + relationToMap(scenarioByUserName.keySet, + (l,r) => + l != r && ( + scenarioId(scenarioByUserName(l)) == scenarioId(scenarioByUserName(r)) + || exclusive(scenarioId(scenarioByUserName(l))).contains(scenarioId(scenarioByUserName(r))) + || scenarioSW(l).flatMap(sw => swExclusive.get(sw).getOrElse(Set.empty)).intersect(scenarioSW(r)).nonEmpty + ) + ) + } +} diff --git a/src/main/scala/views/interference/model/specification/InterferenceSpecification.scala b/src/main/scala/views/interference/model/specification/InterferenceSpecification.scala new file mode 100644 index 0000000..4b965ff --- /dev/null +++ b/src/main/scala/views/interference/model/specification/InterferenceSpecification.scala @@ -0,0 +1,373 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package views.interference.model.specification + +import pml.model.configuration.TransactionLibrary +import pml.model.hardware.{Hardware, Platform} +import pml.model.service.Service +import pml.operators._ +import views.interference.model.specification.InterferenceSpecification.{PhysicalScenario, PhysicalScenarioId, PhysicalTransaction, PhysicalTransactionId} +import views.interference.operators.Transform + +/** + * Base trait for all interference specification + */ +trait InterferenceSpecification + extends Transform.BasicInstances { + self: Platform => + + /** + * Map from the physical transaction id and their service sequence representation + * computed through an analysis of the platform + * WARNING: this lazy variable MUST NOT be called during platform object initialisation + * @group transaction_relation + */ + final lazy val purifiedTransactions: Map[PhysicalTransactionId, PhysicalTransaction] = + this.transactionsByName.transform((k, _) => purify(k)) + + /** + * Map from the service sequence representation to their id + * WARNING: this lazy variable MUST NOT be called during platform object initialisation + * @group transaction_relation + */ + final lazy val purifiedTransactionsName: Map[PhysicalTransaction, PhysicalTransactionId] = + purifiedTransactions.groupMapReduce(_._2)(_._1)((l, _) => l) + + /** + * compute the considered scenarios depending on the configuration and the (optional) library, a scenario is either: + * a named and used transaction (e.g. val t = Transaction(a read b); t.used) + * a named and used scenario (e.g. val s = Scenario(t1, t2); s.used) + * an anonymous copy (e.g. a copy r on s) + * an anonymous transaction (e.g. a read b) not already involved in a copy, a named scenario or a named transaction + * WARNING this will discard an anonymous transaction defined inside and outside a copy, + * this issue does not occur if we keep the segregation Smart/NonSmart + * @group scenario_relation + * @return the set of scenarios + */ + final lazy val purifiedScenarios: Map[PhysicalScenarioId, PhysicalScenario] = this match { + case l: TransactionLibrary => + val namedScenarios = l.scenarioByUserName + .map(kv => scenarioId(kv._2) -> kv._2) + val anonymousTransaction = purifiedTransactions + .filter(kv => !namedScenarios.values.exists(_.contains(kv._1))) + .map(kv => PhysicalScenarioId(kv._1.id) -> Set(kv._1)) + namedScenarios ++ anonymousTransaction + case _ => + val anonymousTransaction = purifiedTransactions + .map(kv => PhysicalScenarioId(kv._1.id) -> Set(kv._1)) + anonymousTransaction + } + + /** + * Check whether a transaction is discarded during the analysis + * @group transparent_predicate + * @param t the identifier of the transaction + * @return true is the transaction is discarded + */ + def isTransparentTransaction(t: PhysicalTransactionId): Boolean + + /** + * Provide the services a given transaction interfere with (additionally to the one identified in its path) + * @group interfere_predicate + * @param t the identifier of the transaction + * @return a set of services + */ + def transactionInterfereWith(t: PhysicalTransactionId): Set[Service] + + /** + * Provide the services a given transaction do not interfere with + * @group interfere_predicate + * @param t the identifier of the transaction + * @return a set of services + */ + def transactionNotInterfereWith(t: PhysicalTransactionId): Set[Service] + + /** + * Check whether two hardware cannot work simultaneously + * @group interfere_predicate + * @param l the left hardware + * @param r the right hardware + * @return true if they cannot work simultaneously + */ + final def finalInterfereWith(l: Hardware, r: Hardware): Boolean = + antiReflexive(l, r) && symmetric[Hardware](interfereWith)(l, r) + + /** + * Check whether two transaction will not occur simultaneously + * @group exclusive_predicate + * @param l the left transaction + * @param r the right transaction + * @return true if they cannot occur simultaneously + */ + final def finalExclusive(l: PhysicalTransactionId, r: PhysicalTransactionId): Boolean = { + antiReflexive(l, r) && symmetric[PhysicalTransactionId](exclusiveWith)(l, r) + } + + /** + * Check whether two scenarios will not occur simultaneously + * @group exclusive_predicate + * @param l the left scenarios + * @param r the right scenarios + * @return true if they cannot occur simultaneously + */ + final def finalExclusive(l: PhysicalScenarioId, r: PhysicalScenarioId): Boolean = + antiReflexive(l, r) && + symmetric((le: PhysicalScenarioId, re: PhysicalScenarioId) => + purifiedScenarios(le).exists(t => + purifiedScenarios(re).exists(tp => + finalExclusive(t, tp))))(l, r) + + /** + * Provide the map encoding of finalInterfereWith + * @group exclusive_predicate + * @param s the set of scenario + * @return the map encoding + */ + final def finalExclusive(s: Set[PhysicalScenarioId]): Map[PhysicalScenarioId, Set[PhysicalScenarioId]] = + relationToMap(s, (l, r) => finalExclusive(l, r)) + + //TODO Very dirty, should consider that an affect is a scenario + /** + * Add the services of transactionInterfereWith to the path and remove the ones of transactionNotInterfereWith + * @group utilFun + * @param t the identifier of the transaction + * @return the path of the transaction + */ + final def purify(t: PhysicalTransactionId): PhysicalTransaction = transactionsByName.get(t) match { + case Some(h :: tail) => + (h +: (transactionInterfereWith(t).toList.sortBy(_.name.name) ++ tail)).filterNot(transactionNotInterfereWith(t)) + case _ => Nil + } + + /** + * Provide the equivalence classes over s with [[views.interference.operators.Equivalent.Ops]] relation + * @group equivalence_predicate + * @param s the set of [[pml.model.service.Service]] + * @return the equivalence classes + */ + final def serviceEquivalenceClasses(s: Set[Service]): Set[Set[Service]] = + equivalenceClasses[Service]((le, re) => reflexive(le, re) || symmetric(areEquivalent)(le, re))(s) + + /** + * Provide the equivalence classes over s with [[views.interference.operators.Equivalent.Ops]] relation + * @group equivalence_predicate + * @param s the set of [[views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId]] + * @return the equivalence classes + */ + final def equivalenceTransactionClasses(s: Set[PhysicalTransactionId]): Set[Set[PhysicalTransactionId]] = { + val serviceClasses = serviceEquivalenceClasses(s.flatMap(transactionsByName.get).flatMap(_.toSet)) + val areEquivalentTr = (l: PhysicalTransactionId, r: PhysicalTransactionId) => { + transactionsByName.contains(l) && + transactionsByName.contains(r) && + transactionsByName(l).size == transactionsByName(r).size && + transactionsByName(l) + .zip(transactionsByName(r)) + .forall(p => serviceClasses.exists(c => c.contains(p._1) && c.contains(p._2))) + } + + equivalenceClasses[PhysicalTransactionId]((le, re) => reflexive(le, re) || symmetric(areEquivalentTr)(le, re))(s) + } + + /** + * Check if it exists at least one common service used by two set of scenarios + * @group utilFun + * @param l the left set of scenarios + * @param r the right set of scenarios + * @return true whether one channel exists + */ + final def channelNonEmpty(l: Set[PhysicalScenarioId], r: Set[PhysicalScenarioId]): Boolean = + l.flatMap(purifiedScenarios) + .flatMap(purifiedTransactions) + .exists(ls => + r.flatMap(purifiedScenarios) + .flatMap(purifiedTransactions) + .exists(rs => ls == rs || finalInterfereWith(ls, rs))) + + /** + * Provide the map encoding of channelNonEmpty + * @group utilFun + * @param s the set of gathered scenarios + * @return the map encoding + */ + final def channelNonEmpty(s: Set[Set[PhysicalScenarioId]]): Map[Set[PhysicalScenarioId], Set[Set[PhysicalScenarioId]]] = + relationToMap(s, (l, r) => channelNonEmpty(l, r)) + + /** + * Check if two services interfere with each other + * @group interfere_predicate + * @param l the left service + * @param r the right service + * @return true if they interfere + */ + final def finalInterfereWith(l: Service, r: Service): Boolean = { + antiReflexive(l, r) && ( + symmetric[Service](interfereWith)(l, r) || + (l.hardwareOwner.nonEmpty && + r.hardwareOwner.nonEmpty && + l.hardwareOwner.exists(ol => r.hardwareOwner.exists(or => finalInterfereWith(ol, or))) //service owner are exclusive + ) + ) + } + + /** + * Check if two services are equivalent + * @group equivalence_predicate + * @param l the left service + * @param r the right service + * @return true if the services are equivalent + */ + protected def areEquivalent(l: Service, r: Service): Boolean + + /** + * Check whether two transaction will not occur simultaneously + * @group exclusive_predicate + * @param l the left transaction + * @param r the right transaction + * @return true if they cannot occur simultaneously + */ + protected def exclusiveWith(l: PhysicalTransactionId, r: PhysicalTransactionId): Boolean + + /** + * Check if two services interfere with each other + * @group interfere_predicate + * @param l the left service + * @param r the right service + * @return true if they interfere + */ + protected def interfereWith(l: Service, r: Service): Boolean + + /** + * Check whether two hardware cannot work simultaneously + * @group interfere_predicate + * @param l the left hardware + * @param r the right hardware + * @return true if they cannot work simultaneously + */ + protected def interfereWith(l: Hardware, r: Hardware): Boolean + + protected def relationToMap[T](all: => Set[T], r: (T, T) => Boolean): Map[T, Set[T]] = + all.groupMapReduce(t => t)(t => all.filter(r(t, _)))(_ ++ _) + + private def reflexive[T](l: T, r: T): Boolean = l == r + + private def symmetric[T](relation: (T, T) => Boolean)(l: T, r: T): Boolean = + relation(l, r) || relation(r, l) + + private def antiReflexive[T](l: T, r: T): Boolean = l != r + + private def addToClosure[T](relation: (T, T) => Boolean)(s: T, closures: Set[Set[T]]): Set[Set[T]] = + if (closures.exists(_.exists(cs => relation(s, cs)))) { + closures + .map(c => if (c.exists(cs => relation(s, cs))) c + s else c) + .foldLeft(Set.empty[Set[T]])((acc, c) => { + if (acc.exists(c2 => c.intersect(c2).nonEmpty)) + acc.map(c2 => if (c.intersect(c2).nonEmpty) c2 ++ c else c2) + else + acc + c + }) + } + else + closures + Set(s) + + private def equivalenceClasses[T](relation: (T, T) => Boolean)(in: Set[T]): Set[Set[T]] = { + val closure = addToClosure[T](relation) _ + in.foldLeft(Set.empty[Set[T]])((acc, s) => closure(s, acc)) + } + + @deprecated("Poor performance implementation, consider using MONOSAT problem to identify channel") + final def channel(t: PhysicalTransactionId, in: Set[PhysicalTransactionId]): Set[Service] = + (for {ts <- purifiedTransactions.get(t) + ss = ts.toSet} yield { + in + .flatMap(purifiedTransactions.get) + .flatMap(tp => tp.toSet.intersect(ss)) + }) getOrElse Set.empty + + @deprecated("Poor performance implementation, consider using recursive transaction equivalence classes splitting perhaps implemented within MONOSAT") + final def equivalentTransactionSets(s: Set[PhysicalTransactionId], in: Set[PhysicalTransactionId]): Set[Set[PhysicalTransactionId]] = { + val transactionsClasses = equivalenceTransactionClasses(in) + .flatMap(c => c.intersect(s).map(_ -> c)) + .toMap + s.foldLeft((Set.empty[PhysicalTransactionId], Set.empty[Set[PhysicalTransactionId]]))((acc, t) => { + val (current, result) = acc + val iniChannel = channel(t, current) + if (current.isEmpty) { + (current + t, (transactionsClasses(t) - t).map(Set(_))) + } else { + val r = (current + t, result.flatMap(st => + (transactionsClasses(t) -- st - t) + .filter(tp => channel(tp, st) == iniChannel && st.forall(!finalExclusive(_, tp))) + .map(st + _))) + r + } + })._2 + } +} + +object InterferenceSpecification { + type Path[A] = List[A] + type PhysicalTransaction = Path[Service] + type PhysicalScenario = Set[PhysicalTransactionId] + type Channel = Set[Service] + + trait Id extends Ordered[Id] { + val id: Symbol + + override def toString: String = id.name + + def compare(that: Id): Int = id.name.compare(that.id.name) + } + + case class PhysicalTransactionId(id: Symbol) extends Id + + case class PhysicalScenarioId(id: Symbol) extends Id + + case class PhysicalScenarioSetId(id: Symbol) extends Id + + case class ChannelId(id: Symbol) extends Id + + def scenarioSetId(t: Iterable[PhysicalScenarioId]): PhysicalScenarioSetId = + PhysicalScenarioSetId(Symbol(t.map(_.id.name).toArray.sorted.mkString("< ", " || ", " >"))) + + def channelId(t: Set[Service]): ChannelId = + ChannelId(Symbol(t.map(_.toString).toArray.sorted.mkString("{ ", ", ", " }"))) + + def groupedScenarioLitId(s: Set[PhysicalScenarioId]): PhysicalScenarioSetId = + PhysicalScenarioSetId(Symbol(scenarioSetId(s).id.name.replace(" ", ""))) + + + trait Default extends InterferenceSpecification { + self: Platform => + + def interfereWith(l: Service, r: Service): Boolean = + l.hardwareOwner == r.hardwareOwner + + def areEquivalent(l: Service, r: Service): Boolean = false + + def interfereWith(l: Hardware, r: Hardware): Boolean = false + + def isTransparentTransaction(t: PhysicalTransactionId): Boolean = false + + def transactionInterfereWith(t: PhysicalTransactionId): Set[Service] = Set.empty + + def transactionNotInterfereWith(t: PhysicalTransactionId): Set[Service] = Set.empty + + protected def exclusiveWith(l: PhysicalTransactionId, r: PhysicalTransactionId): Boolean = + l.initiator == r.initiator + } +} diff --git a/src/main/scala/views/interference/model/specification/PhysicalTableBasedInterferenceSpecification.scala b/src/main/scala/views/interference/model/specification/PhysicalTableBasedInterferenceSpecification.scala new file mode 100644 index 0000000..5f724cc --- /dev/null +++ b/src/main/scala/views/interference/model/specification/PhysicalTableBasedInterferenceSpecification.scala @@ -0,0 +1,7 @@ +package views.interference.model.specification + +import pml.model.hardware.Platform + +trait PhysicalTableBasedInterferenceSpecification extends TableBasedInterferenceSpecification { + self: Platform => +} diff --git a/src/main/scala/views/interference/model/specification/TableBasedInterferenceSpecification.scala b/src/main/scala/views/interference/model/specification/TableBasedInterferenceSpecification.scala new file mode 100644 index 0000000..3e5a259 --- /dev/null +++ b/src/main/scala/views/interference/model/specification/TableBasedInterferenceSpecification.scala @@ -0,0 +1,150 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package views.interference.model.specification + +import pml.exporters.FileManager +import pml.model.configuration.TransactionLibrary +import pml.model.hardware.{Hardware, Platform} +import pml.model.service.Service +import pml.model.software.Application +import pml.operators.{Provided, *} +import views.interference.model.relations.* +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +import java.io.FileWriter +import scala.collection.mutable.Set as MSet + +/** + * Trait providing a wide range of modelling features to specify assumption + */ +trait TableBasedInterferenceSpecification extends InterferenceSpecification + with InterfereRelation.Instances + with NotInterfereRelation.Instances + with ExclusiveRelation.GeneralInstances + with ExclusiveRelation.ApplicationInstances + with EquivalenceRelation.Instances + with TransparentSet.Instances { + self: Platform => + + /** + * The set of services provided by the platform + * @group service_relation + */ + final lazy val services: Set[Service] = implicitly[Provided[Platform, Service]].apply(self) + + /** + * Derive implementation from [[physicalTransactionIdInterfereWithService]] + * @param t the identifier of the transaction + * @return a set of services + */ + final def transactionInterfereWith(t: PhysicalTransactionId): Set[Service] = + physicalTransactionIdInterfereWithService.get(t).getOrElse(Set.empty) + + /** + * Derive implementation from [[physicalTransactionIdNotInterfereWithService]] + * @param t the identifier of the transaction + * @return a set of services + */ + final def transactionNotInterfereWith(t: PhysicalTransactionId): Set[Service] = + physicalTransactionIdNotInterfereWithService.get(t).getOrElse(Set.empty) + + /** + * Derive implementation from [[transactionIsTransparent]] + * @param t the identifier of the transaction + * @return true is the transaction is discarded + */ + final def isTransparentTransaction(t: PhysicalTransactionId): Boolean = + transactionIsTransparent.value.contains(t) + + /** + * Derive implementation from [[serviceInterfereWithService]] + * @param l the left service + * @param r the right service + * @return true if they interfere + */ + def interfereWith(l: Service, r: Service): Boolean = + serviceInterfereWithService + .get(l) + .getOrElse(Set.empty[Service]) + .contains(r) + + /** + * Derive implementation from [[hardwareExclusive]] + * @param l the left hardware + * @param r the right hardware + * @return true if they cannot work simultaneously + */ + final def interfereWith(l: Hardware, r: Hardware): Boolean = + hardwareExclusive + .get(l) + .getOrElse(Set.empty[Hardware]) + .contains(r) + + /** + * Derive implementation from [[serviceEquivalent]] + * @param l the left service + * @param r the right service + * @return true if the services are equivalent + */ + final def areEquivalent(l: Service, r: Service): Boolean = + serviceEquivalent + .get(l) + .getOrElse(Set.empty[Service]) + .contains(r) + + /** + * Derive implementation from [[transactionExclusive]], [[swExclusive]] or same hardware owner + * @param l the left transaction + * @param r the right transaction + * @return true if they cannot occur simultaneously + */ + final def exclusiveWith(l: PhysicalTransactionId, r: PhysicalTransactionId): Boolean = { + val tExclusive = transactionExclusive + .get(l) + .getOrElse(Set.empty[PhysicalTransactionId]) + .contains(r) + val samePLUsed = l.initiator.intersect(r.initiator).nonEmpty + val differentAppUsed = Application.all + .subsets(2) + .filter(s => { + val (al, ar) = (s.head, s.last) + val lUsed = s.filter(sw => transactionsBySW(sw).contains(l)) + val rUsed = s.filter(sw => transactionsBySW(sw).contains(r)) + lUsed.contains(al) && rUsed.contains(ar) || lUsed.contains(ar) && rUsed.contains(al) + }) + val appExclusive = differentAppUsed.nonEmpty && differentAppUsed.forall(s => { + val (al, ar) = (s.head, s.last) + swExclusive.get(al).getOrElse(MSet.empty[Application]).contains(ar) + }) + samePLUsed || tExclusive || appExclusive + } +} + +object TableBasedInterferenceSpecification { + + /** + * Default implementation of [[TableBasedInterferenceSpecification]] + */ + trait Default extends TableBasedInterferenceSpecification { + self: Platform with TransactionLibrary => + + final override def interfereWith(l: Service, r: Service): Boolean = + l.hardwareOwner == r.hardwareOwner || super.interfereWith(l, r) + + } +} diff --git a/src/main/scala/views/interference/model/specification/package.scala b/src/main/scala/views/interference/model/specification/package.scala new file mode 100644 index 0000000..f9f94f2 --- /dev/null +++ b/src/main/scala/views/interference/model/specification/package.scala @@ -0,0 +1,15 @@ +package views.interference.model + +/** + * Package containing the modelling features used to specify the assumptions considered during the interference analysis. + * + * @see [[ApplicativeTableBasedInterferenceSpecification]] provides modelling features to specify application related assumption + * Example can be found in [[views.interference.examples.simpleKeystone.SimpleKeystoneApplicativeTableBasedInterferenceSpecification]] + * @see [[PhysicalTableBasedInterferenceSpecification]] provides modelling features to specify hardware related assumption + * Example can be found in [[views.interference.examples.simpleKeystone.SimpleKeystonePhysicalTableBasedInterferenceSpecification]] + * @see [[TableBasedInterferenceSpecification]] provides a wide range of modelling features to specify assumption. + * @see [[InterferenceSpecification]] provides the basic modelling features to specify the assumptions, if possible consider using + * @see [[InterferenceSpecification.Default]] provides a default specialization of [[InterferenceSpecification]] + * @see [[TableBasedInterferenceSpecification.Default]] provides a default specialization of [[TableBasedInterferenceSpecification]] + */ +package object specification diff --git a/src/main/scala/views/interference/operators/Analyse.scala b/src/main/scala/views/interference/operators/Analyse.scala new file mode 100644 index 0000000..bf630c7 --- /dev/null +++ b/src/main/scala/views/interference/operators/Analyse.scala @@ -0,0 +1,667 @@ +package views.interference.operators + +import monosat.* +import monosat.Logic.* +import net.sf.javabdd.BDD +import pml.exporters.FileManager +import pml.model.configuration.TransactionLibrary +import pml.model.configuration.TransactionLibrary.UserScenarioId +import pml.model.hardware.{Hardware, Platform} +import pml.model.service.Service +import pml.model.utils.Message +import pml.operators.* +import scalaz.Memo.immutableHashMapMemo +import views.interference.model.formalisation.* +import views.interference.model.formalisation.ProblemElement.* +import views.interference.model.specification.InterferenceSpecification.* +import views.interference.model.specification.{ApplicativeTableBasedInterferenceSpecification, InterferenceSpecification} + +import java.io.{File, FileWriter} +import scala.collection.immutable.SortedMap +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.* +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} +import scala.jdk.CollectionConverters.* +import scala.language.postfixOps + +/** + * Base trait providing proof that an element is analysable with monosat + * @tparam T the type of the component (contravariant) + */ +private[operators] trait Analyse[-T] { + def computeInterference( + x:T, + maxSize: Int, + ignoreExistingAnalysisFiles: Boolean, + verboseResultFile: Boolean): Future[Set[File]] + + def printGraph(platform: T): File + +} + +object Analyse { + + type ConfiguredPlatform = Platform & InterferenceSpecification + + type ConfiguredLibraryBasedPlatform = ConfiguredPlatform + & ApplicativeTableBasedInterferenceSpecification + & TransactionLibrary + + /* ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element x of type T can be analysed then + * + * To provide a computation future (computation is not executed yet) of all interference up to maxSize + * {{{x.computeKInterference(maxSize)}}} + * To actually perform the interference calculus up to maxSize + * {{{x.computeKInterference(maxSize, duration)}}} + */ + trait Ops { + + /** + * Extension method + */ + extension[T<: Platform](self:T) { + + /** + * Provide a computation future (computation is not executed yet) of all interference up to maxsize + * @param maxSize the maximal number of initiator that can execute a transaction, + * it should be less or equal to the total number of initiators in the platform. + * @param ignoreExistingAnalysisFiles do the analysis only even if result files for the considered platform + * can be found in the analysis directory + * @param verboseResultFile add extra information on analysis files + * @param ev the proof that the component is analysable + * @return the computation future + */ + def computeKInterference(maxSize: Int, + ignoreExistingAnalysisFiles: Boolean, + verboseResultFile: Boolean)(using ev:Analyse[T]): Future[Set[File]] = + ev.computeInterference(self,maxSize,ignoreExistingAnalysisFiles,verboseResultFile) + + /** + * Perform the interference analysis + * @param maxSize the maximal number of initiator that can execute a transaction, + * it should be less or equal to the total number of initiators in the platform. + * @param timeout the maximal duration that is allowed to perform the interference computation. + * @param ignoreExistingAnalysisFiles do the analysis only even if result files for the considered platform + * can be found in the analysis directory (false by default) + * @param verboseResultFile add extra information on analysis files (false by default) + * @param ev the proof that the component is analysable + * @return the computation future + */ + def computeKInterference(maxSize: Int, + timeout: Duration, + ignoreExistingAnalysisFiles: Boolean = false, + verboseResultFile: Boolean = false + )(using ev: Analyse[T]): Set[File] = + Await.result(ev.computeInterference(self, maxSize, ignoreExistingAnalysisFiles, verboseResultFile),timeout) + + /** + * Perform the interference analysis considering that all the initiators can execute a transaction + * @param timeout the maximal duration that is allowed to perform the interference computation. + * @param ignoreExistingAnalysisFiles do the analysis only even if result files for the considered platform + * can be found in the analysis directory (false by default) + * @param verboseResultFile add extra information on analysis files (false by default) + * @param ev the proof that the component is analysable + * @return the computation future + */ + def computeAllInterference(timeout: Duration, + ignoreExistingAnalysisFiles: Boolean = false, + verboseResultFile: Boolean = false + )(using ev: Analyse[T], p:Provided[T,Hardware]): Set[File] = + Await.result(ev.computeInterference(self, self.initiators.size, ignoreExistingAnalysisFiles, verboseResultFile),timeout) + } + } + + /* ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * A platform is analysable + */ + given Analyse[ConfiguredPlatform] with { + + def printGraph(platform: ConfiguredPlatform): File = { + val problem = computeProblemConstraints(platform, platform.initiators.size) + val result = FileManager.exportDirectory.getFile(s"${platform.name.name}_graph.dot") + val graphWriter = new FileWriter(result) + val dummySolver = new Solver() + graphWriter.write(problem.serviceGraph.toGraph(dummySolver).draw()) + dummySolver.close() + graphWriter.close() + result + } + + + /** + * Sequential version of MONOSAT-based interference computation + * The parallelization is impossible, MONOSAT is tied to a native library so cannot ensure that parallel calls + * are safe (TOO BAD) + * + * @param platform the platform for which the interference are computed, must contained the extra information + * for the interference calculus + * @return the files containing the results of the interference calculus + */ + def computeInterference( + platform: ConfiguredPlatform, + maxSize: Int, + ignoreExistingAnalysisFiles: Boolean, + verboseResultFile: Boolean + ): Future[Set[File]] = Future { + val sizes = 2 to maxSize + val interferenceFiles = sizes.map(size => size -> FileManager.analysisDirectory.getFile(s"${platform.fullName}_itf_$size.txt")).toMap + val freeFiles = sizes.map(size => size -> FileManager.analysisDirectory.getFile(s"${platform.fullName}_free_$size.txt")).toMap + val channelFiles = sizes.map(size => size -> FileManager.analysisDirectory.getFile(s"${platform.fullName}_channel_$size.txt")).toMap + val files = (interferenceFiles.values ++ freeFiles.values ++ channelFiles.values).toSet + if (!ignoreExistingAnalysisFiles && files.forall(f => FileManager.analysisDirectory.locate(f.getName).isDefined)) { + println(Message.analysisResultFoundInfo(FileManager.analysisDirectory.name, platform.fullName)) + files + } else { + val generateModelStart = System.currentTimeMillis() + val problem = computeProblemConstraints(platform, maxSize) + val summaryFile = FileManager.analysisDirectory.getFile(s"${platform.fullName}_itf_calculus_summary.txt") + val summaryWriter = new FileWriter(summaryFile) + val interferenceWriters = interferenceFiles.transform((_, v) => new FileWriter(v)) + val freeWriters = freeFiles.transform((_, v) => new FileWriter(v)) + val channelWriters = channelFiles.transform((_, v) => new FileWriter(v)) + val allWriters = channelWriters.values ++ freeWriters.values ++ interferenceWriters.values + + if (verboseResultFile) { + channelWriters.foreach(kv => writeChannelInfo(kv._2, kv._1)) + freeWriters.foreach(kv => writeFreeInfo(kv._2, kv._1)) + interferenceWriters.foreach(kv => writeITFInfo(kv._2, kv._1)) + allWriters.foreach(w => writeFileInfo(w, platform)) + } + + val nbFree = mutable.Map.empty[Int, Int].withDefaultValue(0) + val nbITF = mutable.Map.empty[Int, Int].withDefaultValue(0) + val channels = mutable.Map.empty[Int, Map[Channel, Int]] + + val update = (isFree: Boolean, + physical: Set[Set[PhysicalScenarioId]], + user: Map[Set[PhysicalScenarioId], Set[Set[UserScenarioId]]]) => { + val userBySize = user.values.flatten.groupBy(_.size).transform((_, v) => v.toSet) + if (isFree) { + updateNumber(nbFree, userBySize) + updateResultFile(freeWriters, userBySize) + } else { + updateNumber(nbITF, userBySize) + updateResultFile(interferenceWriters, userBySize) + updateChannelNumber(problem, channels, physical, user) + } + } + + println(Message.successfulModelBuildInfo(platform.fullName, (System.currentTimeMillis().toFloat - generateModelStart) * 1E-3)) + + println(Message.startingNonExclusiveScenarioEstimationInfo(platform.fullName)) + val estimateNonExclusiveScenarioStart = System.currentTimeMillis().toFloat + val nonExclusiveScenarios = getNonExclusiveScenarioSetCard(platform, sizes.max) + println(Message.successfulNonExclusiveScenarioEstimationInfo(platform.fullName, + (System.currentTimeMillis().toFloat - estimateNonExclusiveScenarioStart) * 1E-3)) + for {(k, v) <- problem.litToNodeSet + isFree = v.forall(_.isEmpty) + physical = problem.decodeModel(Set(k), isFree) if physical.nonEmpty + userDefined = physical.groupMapReduce(p => p)(problem.decodeUserModel)(_ ++ _)} + update(isFree, physical, userDefined) + + val assessmentStartDate = System.currentTimeMillis() + + println(Message.iterationCompletedInfo(1, sizes.max, + (System.currentTimeMillis() - assessmentStartDate) * 1E-3)) + for(size <- sizes) { + assert(nbITF(size) <= nonExclusiveScenarios(size), s"[ERROR] Interference analysis is unsound, the number of $size-itf is greater thant $size-scenarios") + assert(nbFree(size) <= nonExclusiveScenarios(size), s"[ERROR] Interference analysis is unsound, the number of $size-free is greater thant $size-scenarios") + } + println(Message.iterationResultsInfo(isFree = false, nbITF, nonExclusiveScenarios)) + println(Message.iterationResultsInfo(isFree = true, nbFree, nonExclusiveScenarios)) + + for (size <- sizes) { + val iterationStartDate = System.currentTimeMillis() + val s = problem.instantiate(size) + val variables = problem.groupedScenarios.transform((k, _) => k.toLit(s)) + while (s.solve()) { + val cube = variables.filter(_._2.value()) + val physical = problem.decodeModel(cube.keySet, problem.isFree.toLit(s).value()) + val userDefined = physical + .groupMapReduce(p => p)(problem.decodeUserModel)(_ ++ _) + update(problem.isFree.toLit(s).value(), physical, userDefined) + s.assertTrue(not(monosat.Logic.and(cube.values.toSeq.asJava))) + } + s.close() + println(Message.iterationCompletedInfo(size,sizes.max, (System.currentTimeMillis() - iterationStartDate) * 1E-3)) + if (size == 2) + assert(nbITF(2) + nbFree(2) == nonExclusiveScenarios(2), "[ERROR] Interference analysis is unsound, the sum of 2-itf and 2-free is not equal to 2-scenarios") + assert(nbITF(size) <= nonExclusiveScenarios(size), s"[ERROR] Interference analysis is unsound, the number of $size-itf is greater thant $size-scenarios") + assert(nbFree(size) <= nonExclusiveScenarios(size), s"[ERROR] Interference analysis is unsound, the number of $size-free is greater thant $size-scenarios") + println(Message.iterationResultsInfo(isFree = false, nbITF, nonExclusiveScenarios)) + println(Message.iterationResultsInfo(isFree = true, nbFree, nonExclusiveScenarios)) + } + val computationTime = (System.currentTimeMillis() - assessmentStartDate) * 1E-3 + updateChannelFile(channelWriters, channels) + + if (verboseResultFile) { + for ((i, w) <- interferenceWriters) { + writeFooter(w, computationTime, nbITF.getOrElse(i, 0)) + } + for ((i, w) <- freeWriters) { + writeFooter(w, computationTime, nbFree.getOrElse(i, 0)) + } + } + + writeFileInfo(summaryWriter, platform) + summaryWriter.write("Computed ITF\n") + summaryWriter.write(Message.printScenarioNumber(nbITF, nonExclusiveScenarios)) + summaryWriter.write("Computed ITF-free\n") + summaryWriter.write(Message.printScenarioNumber(nbFree, nonExclusiveScenarios)) + writeFooter(summaryWriter, computationTime) + + for (w <- allWriters ++ List(summaryWriter)) { + w.flush() + w.close() + } + println(Message.analysisCompletedInfo("Interference analysis",computationTime)) + files + } + } + + private def undirectedEdgeId(l: MNode, r: MNode): EdgeId = Symbol(List(l, r).map(_.id.name).sorted.mkString("--")) + + private def nodeId(s: Set[Service]): NodeId = Symbol(s.toList.map(_.toString).sorted.mkString("<", "$", ">")) + + /** + * Definition of the core problem without cardinality constraints on interference sets + * + * @param platform the platform on which the interference analysis is performed + * @return the variables and constraints to be instantiated in a MONOSAT Solver + */ + private def computeProblemConstraints( + platform: ConfiguredPlatform, + maxSize: Int): Problem = { + + // Utilitarian functions + val addNode: Set[Service] => MNode = immutableHashMapMemo { ss => MNode(nodeId(ss)) } + + val addLit: LitId => MLit = immutableHashMapMemo { s => MLit(s) } + + //DEFINITION OF VARIABLES + + // retrieving simple transactions to consider from the platform + val transactions = platform.purifiedTransactions + + val idToScenario = platform.purifiedScenarios + + val scenarioToLit = idToScenario + .transform((k, _) => MLit(Symbol(k.id.name + "_sn"))) + + // association of the simple transaction path to its formatted name + val initialPathT = idToScenario.to(SortedMap) + + // all the services used by transactions + val initialServices = initialPathT.values + .flatMap(s => s.map(transactions)) + .toSet + .flatten + + // all the services exclusive to a given one + val initialServicesInterfere = initialServices + .map(s => s -> initialServices.filter(s2 => platform.finalInterfereWith(s, s2))) + .toMap + + // the actual path will be the service that can be a channel, ie + // a service that is a service (or exclusive to a service) of a different and non exclusive transaction + val pathT: Map[PhysicalScenarioId, Set[PhysicalTransaction]] = initialPathT + .view + .mapValues(s => + s.map(t => + transactions(t).filter(s => + transactions.keySet.exists(t2 => + t != t2 && + !platform.finalExclusive(t, t2) && + transactions(t2).exists(s2 => s2 == s || initialServicesInterfere(s2).contains(s)))))) + .toMap + + val services = pathT.values.toSet.flatten.flatten + + val servicesExclusive = services.map(s => s -> services.filter(s2 => platform.finalInterfereWith(s, s2))).toMap + + // the nodes of the service graph are the services grouped by exclusivity pairs + val serviceToNodes = servicesExclusive.transform((k, v) => + if (v.isEmpty) + Set(addNode(Set(k))) + else + v.map(k2 => addNode(Set(k, k2))) + ) + + val nodeToServices = serviceToNodes + .keySet + .flatMap(k => serviceToNodes(k).map(_ -> k)) + .groupMap(_._1)(_._2) + + val reducedNodePath = pathT + .transform((_, v) => v.map(t => t.map(serviceToNodes))) + + val scenarioToGroupedLit = reducedNodePath + .groupMap(_._2)(_._1) + .values + .flatMap(ss => ss.map(_ -> ss)) + .groupMapReduce(_._1)(x => addLit(groupedScenarioLitId(x._2.toSet).id))((l, _) => l) + + val groupedLitToScenarios = scenarioToGroupedLit + .groupMap(_._2)(_._1) + .transform((_, v) => v.toSet) + + val groupedLitToNodeSet = scenarioToGroupedLit + .groupMapReduce(_._2)(kv => reducedNodePath(kv._1).map(_.toSet.flatten))((l, _) => l) + + val addUndirectedEdgeI: Set[MNode] => MEdge = immutableHashMapMemo { + lr => MEdge(lr.head, lr.last, undirectedEdgeId(lr.head, lr.last)) + } + + val addUndirectedEdge = (lr: Set[Service]) => + serviceToNodes(lr.head).flatMap(n1 => serviceToNodes(lr.last).map(n2 => addUndirectedEdgeI(Set(n1, n2)))) + + // an edge is added to service graph iff one of transaction use it: + // * the transaction must not be transparent + // * the edge must not contain a service considered as non impacted + val edgesToScenarios: Map[MEdge, Set[PhysicalScenarioId]] = pathT + .keySet + .flatMap(s => + pathT(s) + .filter(_.size > 1) + .flatMap(t => + t.sliding(2).flatMap(slice => addUndirectedEdge(slice.toSet).map(_ -> s)) + ) + ) + .groupMap(_._1)(_._2) + + val graph = MGraph(nodeToServices.keySet, edgesToScenarios.keySet) + + // DEFINITION OF CONSTRAINTS + + //an edge is enabled iff at least one of the transactions using is selected + val edgeCst = edgesToScenarios + .transform((k, trs) => SimpleAssert(Equal(MEdgeLit(k, graph), Or(trs.map(scenarioToGroupedLit).toSeq: _*)))) + + // there is an interference if the remaining graph is connected, + // here translated as at least one service used by other transactions is reachable from + // the initiator service of another one + + val (trivialFreeScenarios, otherScenarios) = scenarioToGroupedLit + .values + .toSet + .partition(groupedLitToNodeSet(_).forall(_.isEmpty)) //All paths of the scenarios are only private ones + + val otherScenariosCouples = otherScenarios + .subsets(2) + .map(ss => { + val (s, sp) = (ss.head, ss.last) + // find a non private service of the left scenario + val sHeads = groupedLitToNodeSet(s).filter(_.nonEmpty).map(_.head) + val spHeads = groupedLitToNodeSet(sp).filter(_.nonEmpty).map(_.head) + (s -> sHeads, sp -> spHeads) + } + ) + .toSet + + val isITF = And( + (trivialFreeScenarios.map(v => Not(v)) + ++ otherScenariosCouples + .map(ss => { + val (vs -> sHeads, vsp -> spLasts) = ss + Implies(And(vs, vsp), Or(sHeads.flatMap(head => spLasts.map(last => Reaches(graph, head, last))).toSeq: _*)) + })).toSeq: _*) + + val isFree = And( + otherScenariosCouples.map(ss => { + val (vs -> sHeads, vsp -> spLasts) = ss + Implies(And(vs, vsp), Not(Or(sHeads.flatMap(head => spLasts.map(last => Reaches(graph, head, last))).toSeq: _*))) + }).toSeq: _*) + + //for each scenario, the scenarios that are exclusive with it + val exclusiveScenarios = idToScenario + .transform((k, _) => idToScenario.keySet.filter(kp => k != kp && platform.finalExclusive(k, kp))) + + //for each grouped scenario variable v, the other variables containing only scenario that are exclusive with all + //scenario of v, thus these variables are exclusive + + val onSnPerGrouped = groupedLitToScenarios.transform((k, v) => Implies(k, Or(v.map(scenarioToLit).toSeq: _*))) + val nonExclusiveSn = scenarioToLit.transform((k, v) => exclusiveScenarios(k).map(scenarioToLit).map(sLit => Implies(sLit, Not(v)))) + + val nonExclusive = (onSnPerGrouped.values ++ nonExclusiveSn.values.flatten).map(SimpleAssert.apply) + + val serviceToScenarioLit = idToScenario + .keySet + .flatMap(k => idToScenario(k).flatMap(tr => transactions(tr).map(_ -> k))) + .groupMap(_._1)(kv => kv._2) + .transform((_, v) => v) + + Problem( + platform, + groupedLitToScenarios, + groupedLitToNodeSet, + idToScenario, + exclusiveScenarios, + graph, + isFree, + isITF, + Set.empty, + edgeCst.values.toSet ++ nonExclusive, + nodeToServices, + serviceToScenarioLit, + Some(maxSize) + ) + } + + private def writeFooter(writer: FileWriter, computationTime: Double, size: Int = -1): Unit = + writer write + s"""------------------------------------------ + ${if (size > -1) s"|Total: $size\n" else ""} + |Computation time: ${computationTime}s + |-------------------------------------------""".stripMargin + + private def writeChannelInfo(writer: FileWriter, size: Int): Unit = + writer write + s"""------------------------------------------ + |Interference channels for $size-ary interferences + |""".stripMargin + + private def writeITFInfo(writer: FileWriter, size: Int): Unit = + writer write + s"""------------------------------------------ + |$size-ary interference transactions + |""".stripMargin + + private def writeFreeInfo(writer: FileWriter, size: Int): Unit = + writer write + s"""------------------------------------------ + |$size-ary interference free transactions + |""".stripMargin + + private def writeFileInfo(writer: FileWriter, platform: Platform & InterferenceSpecification): Unit = + writer write + s"""Platform Name: ${platform.name.name} + |File: ${platform.sourceFile.value} + |Date: ${java.time.LocalDateTime.now().toString} + |------------------------------------------ + |""".stripMargin + + private def updateChannelFile(writer: Map[Int, FileWriter], channels: mutable.Map[Int, Map[Channel, Int]]): Unit = { + for ((k, v) <- channels) + writer(k) + .write(v.map(c => s"${channelId(c._1)}: ${c._2} interferences").toList.sorted.mkString("\n")) + } + + + private def updateResultFile(writer: Map[Int, FileWriter], m: Map[Int, Set[Set[UserScenarioId]]]): Unit = { + for ((k, v) <- m; ss <- v) + writer(k).write(s"${scenarioSetId(ss.map(s => PhysicalScenarioId(s.id)))}\n") + } + + private def updateNumber(nbITF: mutable.Map[Int, Int], m: Map[Int, Set[Set[UserScenarioId]]]): Unit = { + for ((k, v) <- m) + nbITF(k) = nbITF.getOrElse(k, 0) + v.size + } + + private def updateChannelNumber( + problem: Problem, + channels: mutable.Map[Int, Map[Channel, Int]], + physical: Set[Set[PhysicalScenarioId]], + user: Map[Set[PhysicalScenarioId], Set[Set[UserScenarioId]]] + ): Unit = { + val channelNb = physical + .flatMap(p => user(p).map(u => (problem.decodeChannel(p), u))) + .groupBy(_._2.size) + .transform((_, v) => v.groupMap(_._1)(_._2).transform((_, v) => v.size)) + for ((k, v) <- channelNb) + channels.get(k) match { + case Some(map) => + channels(k) = (map.toSeq ++ v.toSeq).groupMapReduce(_._1)(_._2)(_ + _) + case None => + channels(k) = v + } + } + } + + /** + * Compute the number of possible scenario sets for a given platform, this result can be used to estimate + * the proportion of itf or free scenario sets over all possible sets. It can be used to check + * that 2-ift + 2-free = 2-non-exclusive (for higher cardinalities, the estimation of k-redundant is needed) + * + * @param platform the studied platform + * @return the number of scenario sets per size + */ + def getNonExclusiveScenarioSetCard(platform: ConfiguredPlatform, max: Int): Map[Int, BigInt] = platform match { + case app: (ApplicativeTableBasedInterferenceSpecification & TransactionLibrary) => + getNonExclusiveScenarioSetCardApp(app, max) + case _ => + getNonExclusiveScenarioSetCardNoApp(platform, max) + } + + def getNonExclusiveScenarioSetCardNoApp(platform: ConfiguredPlatform, max: Int): Map[Int, BigInt] = { + val scenario = platform.purifiedScenarios + val exclusive = platform.finalExclusive(scenario.keySet) + val factory = new SymbolBDDFactory() + val bdd = getNonExclusiveKBDD(scenario.keySet.toSeq, exclusive, max, factory) + + // for each cardinality, compute the number of satisfying assignments of the BDD encoding scenario sets + // containing exactly k non exclusive scenarios + val result = platform match { + case l: TransactionLibrary => + val weightMap = scenario + .transform((_, v) => l.scenarioUserName(v)) + .map(kv => kv._1.id -> kv._2.size) + .filter(_._2 >= 1) + bdd.transform((_, v) => factory.getPathCount(v, weightMap)) + case _ => + bdd.transform((_, v) => factory.getPathCount(v)) + } + factory.dispose() + result + } + + def getNonExclusiveKBDD[T](values: Seq[T], exclusive: Map[T, Set[T]], max: Int, factory: SymbolBDDFactory): Map[Int, BDD] = { + val symbols = values.map(x => x -> factory.getVar(Symbol(x.toString))).toMap + + // when a scenario s is selected then at no other scenarios is exclusive with it + // \bigwedge_{s \in scenarioVar} bdd(s) \Rightarrow not \bigvee_{s' \in exclusive(s)} bdd(s') + val isExclusive = factory.andBDD(exclusive.map(p => + symbols(p._1).imp(factory.orBDD(p._2.map(symbols)).not) + )) + + (2 to max).map(k => + k -> factory.mkExactlyK(values.map(x => Symbol(x.toString)), k).and(isExclusive) + ).toMap + } + + def getNonExclusiveScenarioSetCardApp(platform: ConfiguredLibraryBasedPlatform, max: Int): Map[Int, BigInt] = { + val factory = new SymbolBDDFactory() + val result = getNonExclusiveKBDD( + platform.scenarioByUserName.keys.toSeq, + platform.finalUserScenarioExclusive, + max, + factory).transform((_, v) => factory.getPathCount(v)) + factory.dispose() + result + } + + /** + * Compute the number of k-redundant scenario sets for a given platform, it can be used to check that + * for all size, k-free + k-itf + k-redundant = k-non-exclusive + * + * @param platform the platform to analyse + * @param free the interference free scenario sets + * @param itf the interference scenario sets + * @return the number of k-redundant per size + */ + @deprecated("poor performance computation of k-redundant cardinal since based on a building out of free and itf" + + "results that are classically very large") + def getRedundantCard( + platform: ConfiguredPlatform, + free: Set[Set[PhysicalScenarioId]], + itf: Set[Set[PhysicalScenarioId]]): Map[Int, BigInt] = { + val idToScenario = platform.purifiedScenarios + val exclusive = idToScenario.keySet.groupMapReduce(t => t)(t => idToScenario.keySet.filter(platform.finalExclusive(t, _)))(_ ++ _) + val allResults = free ++ itf + val scenarioToScenarioSet = allResults + .flatMap(ss => ss.map(s => s -> ss)) + .groupMap(_._1)(_._2) + val scenarioVar = idToScenario.keys.toSeq + val nonEmptyChannelResults = platform + .channelNonEmpty(allResults) + val factory = new SymbolBDDFactory() + + + val isNonExclusive = exclusive.foldLeft(factory.one())((acc, p) => acc.and( + factory.getVar(p._1.id).imp( + p._2.foldLeft(factory.zero())((orAcc, s) => orAcc.or(factory.getVar(s.id))).not()) + )) + // if s in scenarioVar is selected then at least one free or itf is selected + val sSelect: BDD = factory.andBDD( + scenarioToScenarioSet.map(p => + factory + .getVar(p._1.id) + .imp(factory.orBDD(p._2.map(ss => + factory.getVar(InterferenceSpecification.scenarioSetId(ss).id)))))) + // if a result is selected then all of its scenarios are selected + val resultSelect: BDD = factory.andBDD( + allResults.map(ss => + factory.getVar(InterferenceSpecification.scenarioSetId(ss).id) + .imp(factory.andBDD(ss.map(s => factory.getVar(s.id)))))) + // if an itf is selected then all itfs owning an interference channel with the itf must be discarded + // i.e. all the itfs sharing a common service with itf or using an exclusive service with itf + val emptyChannel = factory.andBDD(nonEmptyChannelResults.map(p => + factory.getVar(InterferenceSpecification.scenarioSetId(p._1).id).imp( + factory.orBDD(p._2.map(ss => factory.getVar(InterferenceSpecification.scenarioSetId(ss).id))).not() + ) + )) + // the selection must contains at least one interference + val atLeastOneITF: BDD = factory.orBDD(itf + .map(InterferenceSpecification.scenarioSetId) + .map(s => factory.getVar(s.id))) + val staticCst = isNonExclusive + .and(sSelect) + .and(resultSelect) + .and(atLeastOneITF) + .and(emptyChannel) + (2 to platform.initiators.size).map(k => + k -> { + val exactlyK = factory.mkExactlyK(scenarioVar.map(_.id), k) + // the selected itf or free must have a cardinality strictly lower than k + val restrictedITFAndFree: BDD = factory.andBDD( + allResults + .filter(ss => ss.size >= k) + .map(InterferenceSpecification.scenarioSetId) + .map(ss => factory.getVar(ss.id).not)) + factory.getPathCount( + exactlyK + .and(restrictedITFAndFree) + .and(staticCst)) + } + ).toMap + } +} diff --git a/src/main/scala/views/interference/operators/Equivalent.scala b/src/main/scala/views/interference/operators/Equivalent.scala new file mode 100644 index 0000000..871e927 --- /dev/null +++ b/src/main/scala/views/interference/operators/Equivalent.scala @@ -0,0 +1,21 @@ +package views.interference.operators + +import views.interference.model.relations.EquivalenceRelation + +private[operators] trait Equivalent[T] { + def equivalent(l:T,r:T):Unit +} + +object Equivalent{ + + trait Ops { + extension[T](l:T){ + def equivalent(r:T)(implicit ev:Equivalent[T]): Unit = ev.equivalent(l,r) + } + } + + given [T] (using ev:EquivalenceRelation[T]): Equivalent[T] with { + def equivalent(l:T, r:T): Unit = ev.add(l,r) + } + +} diff --git a/src/main/scala/views/interference/operators/Exclusive.scala b/src/main/scala/views/interference/operators/Exclusive.scala new file mode 100644 index 0000000..56c350d --- /dev/null +++ b/src/main/scala/views/interference/operators/Exclusive.scala @@ -0,0 +1,24 @@ +package views.interference.operators + +import views.interference.model.relations.ExclusiveRelation + +private[operators] trait Exclusive[T] { + def apply(l:T, r:T): Unit +} + +object Exclusive{ + + /** + * If an element l of type T can be exclusive with another element r of type T, the following operator can be used + * {{{l exclusiveWith r}}} + */ + trait Ops { + extension[T](l:T){ + def exclusiveWith(r:T)(using ev:Exclusive[T]): Unit = ev(l,r) + } + } + + given [T] (using relation: ExclusiveRelation[T]): Exclusive[T] with { + def apply(l: T, r: T): Unit = relation.add(l, r) + } +} diff --git a/src/main/scala/views/interference/operators/Interfere.scala b/src/main/scala/views/interference/operators/Interfere.scala new file mode 100644 index 0000000..b360411 --- /dev/null +++ b/src/main/scala/views/interference/operators/Interfere.scala @@ -0,0 +1,74 @@ +package views.interference.operators + +import pml.model.service.Service +import views.interference.model.relations.{InterfereRelation, NotInterfereRelation} +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +private[operators] trait Interfere[L, R] { + def interfereWith(l: L, r: R): Unit + + def notInterfereWith(l: L, r: R): Unit +} + +object Interfere { + + /** + * If an element x of type L and an element y of type R can interfere then the operator can be used as follows: + * + * x interferes with y + * {{{x interfereWith y}}} + * x does not interfere with y + * {{{x notInterfereWith y}}} + */ + trait Ops { + + extension[L] (self: L) { + def interfereWith[R](that: R)(using ev: Interfere[L, R]): Unit = ev.interfereWith(self, that) + def interfereWith[R](that: Iterable[R])(using ev: Interfere[L, R]): Unit = for {x <- that} ev.interfereWith(self, x) + def notInterfereWith[R](that: R)(using ev: Interfere[L, R]): Unit = ev.notInterfereWith(self, that) + def notInterfereWith[R](that: Iterable[R])(using ev: Interfere[L, R]): Unit = for {x <- that} ev.notInterfereWith(self, x) + } + } + + given[L, R] (using a: InterfereRelation[L, R], n: NotInterfereRelation[L, R]): Interfere[L, R] with { + def interfereWith(l: L, r: R): Unit = a.add(l, r) + + def notInterfereWith(l: L, r: R): Unit = n.add(l, r) + } + + given[LS <: Service, RS <: Service] (using ev: Interfere[Service, Service]): Interfere[LS, RS] with { + def interfereWith(l: LS, r: RS): Unit = ev.interfereWith(l, r) + + def notInterfereWith(l: LS, r: RS): Unit = ev.notInterfereWith(l, r) + } + + given[R <: Service] (using ev: Interfere[PhysicalTransactionId, Service]): Interfere[PhysicalTransactionId, R] with { + def interfereWith(l: PhysicalTransactionId, r: R): Unit = ev.interfereWith(l, r) + + def notInterfereWith(l: PhysicalTransactionId, r: R): Unit = ev.notInterfereWith(l, r) + } + + given[L, RS <: Service] (using + transformation: Transform[L, Set[PhysicalTransactionId]], + ev: Interfere[PhysicalTransactionId, Service]): Interfere[L, RS] with { + def interfereWith(l: L, r: RS): Unit = + for {id <- transformation(l)} + ev.interfereWith(id, r) + + def notInterfereWith(l: L, r: RS): Unit = + for {id <- transformation(l)} + ev.notInterfereWith(id, r) + } + + given[LP, RS <: Service] (using + transformation: Transform[LP, Option[PhysicalTransactionId]], + ev: Interfere[PhysicalTransactionId, Service]): Interfere[LP, RS] with { + def interfereWith(l: LP, r: RS): Unit = + for {id <- transformation(l)} + ev.interfereWith(id, r) + + def notInterfereWith(l: LP, r: RS): Unit = + for {id <- transformation(l)} + ev.notInterfereWith(id, r) + } +} diff --git a/src/main/scala/views/interference/operators/PostProcess.scala b/src/main/scala/views/interference/operators/PostProcess.scala new file mode 100644 index 0000000..543bed2 --- /dev/null +++ b/src/main/scala/views/interference/operators/PostProcess.scala @@ -0,0 +1,345 @@ +/** ***************************************************************************** + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * **************************************************************************** */ + +package views.interference.operators + +import pml.exporters.FileManager +import pml.model.configuration.TransactionLibrary +import pml.model.hardware.{Hardware, Platform} +import pml.model.software.Application +import pml.model.utils.Message +import pml.operators._ +import views.interference.operators.Analyse.ConfiguredPlatform + +import java.io.{File, FileWriter} +import scala.concurrent.ExecutionContext.Implicits._ +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} +import scala.io.{BufferedSource, Source} +import scala.math.Ordering.Implicits._ + +/** + * Base trait providing proof that an element is post processable + * @tparam T the type of the component (contravariant) + */ +private[operators] trait PostProcess[-T] { + def interferenceDiff(x: T, that: Platform): Seq[File] + + def parseITFScenarioFile(x: T): Array[Seq[String]] + + def parseITFScenarioFile(x: T,n: Int): Array[Seq[String]] + + def parseFreeScenarioFile(x: T,n: Int): Array[Seq[String]] + + def sortPLByITFImpact(x: T,max: Option[Int]): Future[Set[File]] + + def sortMultiPathByITFImpact(x: T, max: Option[Int]): Future[Set[File]] + +} + +object PostProcess { + + /* ------------------------------------------------------------------------------------------------------------------ + * EXTENSION METHODS + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * If an element x of type T can be post processed then the operator can be used as follows: + * + * If y is a [[pml.model.hardware.Platform]] then we can compare the interferences computed for x and y + * {{{x.interferenceDiff(y)}}} + * Try to find and parse itf results for the considered element + * {{{x.parseITFScenarioFile()}}} + * Try to find and parse the n-itf results for the considered element + * {{{x.parseITFScenarioFile(n)}}} + * Try to find and parse the n-itf-free results for the considered element + * {{{x.parseFreeScenarioFile(n)}}} + * Compute for each hardware component the number of up to n-itf scenario where the component is involved in the interference channel. + * The result is provided in a file of the analysis directory + * {{{x.sortPLByITFImpact(n)}}} + * Compute for each multi path transaction the number of up to itf scenario involving at least one of its branches + * The result is provided in a file of the analysis directory + * {{{x.sortMultiPathByITFImpact(n)}}} + */ + trait Ops { + + /** + * Extension method class + * + */ + extension[T](self:T) { + + /** + * Compare the interference results with another element and store the result in a dedicated file in the + * analysis folder + * @param that the other element + * @param ev the proof that any element of T is analysable + * @return the location of the result files + */ + def interferenceDiff(that: Platform)(using ev:PostProcess[T]): Seq[File] = ev.interferenceDiff(self,that) + + /** + * Try to find and parse itf results for the considered element + * @param ev the proof that any element of T is analysable + * @return the set of set of scenario identifiers that are interference scenarios + */ + def parseITFScenarioFile()(using ev:PostProcess[T]): Array[Seq[String]] = ev.parseITFScenarioFile(self) + + /** + * Try to find and parse the n-itf results for the considered element + * @param n the maximal size of transaction per itf scenario + * @param ev the proof that any element of T is analysable + * @return the set of set of scenario identifiers that are interference scenarios + */ + def parseITFScenarioFile(n: Int)(using ev:PostProcess[T]): Array[Seq[String]] = ev.parseITFScenarioFile(self,n) + + /** + * Try to find and parse the n-itf-free results for the considered element + * @param n the maximal size of transaction per itf scenario + * @param ev the proof that any element of T is analysable + * @return the set of set of scenario identifiers that are interference free scenarios + */ + def parseFreeScenarioFile(n: Int)(using ev:PostProcess[T]): Array[Seq[String]] = ev.parseFreeScenarioFile(self, n) + + /** + * Compute for each hardware component the number of itf scenario where the component is involved in the interference channel + * The result is provided in a file of the analysis directory + * @param max the optional maximal size of the considered itf + * @param ev the proof that any element of T is analysable + * @return the location of the result files + */ + def sortPLByITFImpact(max: Option[Int])(using ev:PostProcess[T]): Set[File] = + Await.result(ev.sortPLByITFImpact(self, max), Duration.Inf) + + /** + * Compute for each multi path transaction the number of itf scenario involving at least one of its branches + * The result is provided in a file of the analysis directory + * @param max the optional maximal size of the considered itf + * @param ev the proof that any element of T is analysable + * @return the location of the result files + */ + def sortMultiPathByITFImpact(max: Option[Int])(using ev:PostProcess[T]): Set[File] = + Await.result(ev.sortMultiPathByITFImpact(self, max), Duration.Inf) + } + } + + /** ------------------------------------------------------------------------------------------------------------------ + * INFERENCE RULES + * --------------------------------------------------------------------------------------------------------------- */ + + /** + * A platform is post processable + */ + given PostProcess[ConfiguredPlatform] with { + + def interferenceDiff(x: ConfiguredPlatform, that: Platform): Seq[File] = { + for { + size <- 2 to Math.min(x.initiators.size, that.initiators.size) + thisITFFile <- FileManager.analysisDirectory.locate(s"${x.fullName}_itf_$size.txt") + thatITFFile <- FileManager.analysisDirectory.locate(s"${that.fullName}_itf_$size.txt") + } yield { + val file = FileManager.analysisDirectory.getFile(s"${x.fullName}_diff_${that.fullName}_itf_$size.txt") + val sThisITF = Source.fromFile(thisITFFile) + val sThatITF = Source.fromFile(thatITFFile) + val thisITF = parseScenarioFile(sThisITF) + val thatITF = parseScenarioFile(sThatITF) + sThatITF.close() + sThisITF.close() + val thisDiffThat = thisITF.diff(thatITF).sorted + val thatDiffThis = thatITF.diff(thisITF).sorted + val writer = new FileWriter(file) + if (thisDiffThat.isEmpty) + writer.write(s"All itf of ${x.fullName} are in ${that.fullName}\n") + else { + writer.write(s"The following ${thisDiffThat.length} itf of ${x.fullName} are not in ${that.fullName}\n") + thisDiffThat.foreach(d => writer.write(s"$d\n")) + } + if (thatDiffThis.isEmpty) + writer.write(s"All itf of ${that.fullName} are in ${x.fullName}\n") + else { + writer.write(s"The following ${thatDiffThis.length} itf in ${that.fullName} are not in ${x.fullName}\n") + thatDiffThis.foreach(d => writer.write(s"$d\n")) + } + writer.flush() + writer.close() + println(Message.successfulITFDifferenceExportInfo(size, x.fullName, that.fullName, file.getAbsolutePath)) + file + } + } + + def parseITFScenarioFile(x:ConfiguredPlatform): Array[Seq[String]] = + for { + k <- (2 to x.initiators.size).toArray + sc <- parseITFScenarioFile(x,k) + } yield + sc + + def parseITFScenarioFile(x:ConfiguredPlatform, n: Int): Array[Seq[String]] = { + for { + file <- FileManager.analysisDirectory.locate(s"${x.fullName}_itf_$n.txt") + } yield { + val s = Source.fromFile(file) + val result = parseScenarioFile(s) + s.close() + result + } + } getOrElse Array.empty + + def parseFreeScenarioFile(x:ConfiguredPlatform): Array[Seq[String]] = + for { + k <- (2 to x.initiators.size).toArray + sc <- parseFreeScenarioFile(x, k) + } yield + sc + + def parseFreeScenarioFile(x:ConfiguredPlatform, n: Int): Array[Seq[String]] = { + for { + file <- FileManager.analysisDirectory.locate(s"${x.fullName}_free_$n.txt") + } yield { + val s = Source.fromFile(file) + val result = parseScenarioFile(s) + s.close() + result + } + } getOrElse Array.empty + + def sortPLByITFImpact(x: ConfiguredPlatform, max: Option[Int]): Future[Set[File]] = { + x.computeKInterference(max.getOrElse(x.initiators.size), ignoreExistingAnalysisFiles = false, verboseResultFile = false) map { + resultFiles => + resultFiles.filter(_.getName.contains("channel")).map(resultFile => { + val size = resultFile.getName.split("\\D+").filter(_.nonEmpty).last.toInt + val file = FileManager.analysisDirectory.getFile(s"${x.fullName}_HW_importance_factor_itf_$size.txt") + val writer = new FileWriter(file) + writer.write(PLInvolvedInITF(x, resultFile) + .toSeq + .sortBy(-_._2) + .map(p => s"${p._1},${p._2}") + .mkString("\n")) + writer.close() + file + }) + } + } + + private def PLInvolvedInITF(x:ConfiguredPlatform, file: File): Map[Hardware, Int] = { + import x._ + val results = parseChannel(Source.fromFile(file)) + val plByScenario = results + .map(p => p._1.flatMap(s => x.directHardware.filter(_.services.exists(s2 => s2.toString == s))).toSet -> p._2) + x.directHardware.foldLeft(Map.empty[Hardware, Int])((localMap, pl) => { + val impacted = plByScenario.filter(_._1.contains(pl)).map(_._2).sum + if (impacted > 0) + localMap.updated(pl, localMap.getOrElse(pl, 0) + impacted) + else + localMap + }) + } + + def parseChannel(source: BufferedSource): Seq[(Seq[String], Int)] = { + val res = source.getLines() + .filter(_.head == '{') + .map(s => { + val split = s.split(":") + val services = split.head.split("[{, }]").filter(_.nonEmpty).toSeq + val size = split.last.split(" ").filter(_.nonEmpty).head.toInt + services -> size + }) + .toSeq + source.close() + res + } + + def sortMultiPathByITFImpact(x:ConfiguredPlatform, max: Option[Int]): Future[Set[File]] = + x.computeKInterference(max.getOrElse(x.initiators.size), ignoreExistingAnalysisFiles = false, verboseResultFile = false) map { + resultFiles => { + val multiPathsTransactions = x match { + case c: TransactionLibrary => + x.multiPathsTransactions.flatMap(s => { + val transactionNames = s.map(x.transactionsName) + (for { + scenarios <- c.scenarioUserName.get(transactionNames) + if scenarios.nonEmpty + } yield { + scenarios.map(x => Set(x.toString)) + }) getOrElse Set(transactionNames.map(_.toString)) + }) + case _ => + x.multiPathsTransactions.map(_.map(x.transactionsName).map(_.toString)) + } + resultFiles.filter(_.getName.contains("itf")).map(resultFile => { + val size = resultFile.getName.split("\\D+").filter(_.nonEmpty).last.toInt + val file = FileManager.analysisDirectory.getFile(s"${x.fullName}_Routing_importance_factor_itf_$size.txt") + val writer = new FileWriter(file) + writer.write({ + val source = Source.fromFile(file) + val itf = parseScenarioFile(source) + source.close() + multiPathsTransactions.groupMapReduce(k => k)(k => + itf.count(sc => sc.exists(s => k.contains(s))))(_ + _) + .toSeq + .sortBy(-_._2) + .map(p => s"${p._1.mkString("{", ",", "}")},${p._2}") + .mkString("\n") + }) + writer.close() + file + }) + } + } + + @deprecated("this indicator should not be used for now, not useful info") + def sortSWByITFImpact(x: ConfiguredPlatform, max: Option[Int]): Future[File] = { + x.computeKInterference(max.getOrElse(x.initiators.size), ignoreExistingAnalysisFiles = false, verboseResultFile = false) map { + resultFiles => { + val file = FileManager.analysisDirectory.getFile(s"${x.fullName}_SW_importance_factor.txt") + val writer = new FileWriter(file) + writer.write(resultFiles.filter(_.getName.contains("itf")) + .foldLeft(Map.empty[Set[Application], Int])((acc, file) => { + val involved = SWInvolvedInITF(x,file) + (acc.toSeq ++ involved.toSeq).groupMapReduce(_._1)(_._2)(_ + _) + }) + .toSeq + .sortBy(-_._2) + .map(p => s"${p._1.mkString("{", ",", "}")},${p._2}") + .mkString("\n")) + writer.close() + file + } + } + } + + //FIXME If using a transaction library, the ids are UserScenarios and not PhysicalScenarios, need to know the difference + //to retrieve the scenarios used by a sw + private def SWInvolvedInITF(x:ConfiguredPlatform, file: File): Map[Set[Application], Int] = { + val results = parseScenarioFile(Source.fromFile(file)) + val swByScenario = results + .map(_.flatMap(s => x.applications.filter(sw => x.transactionsBySW(sw).exists(s2 => s2.toString == s))).toSet) + swByScenario.groupMapReduce(s => s)(_ => 1)(_ + _) + } + } + + def parseScenarioFile(source: BufferedSource): Array[Seq[String]] = { + val res = source.getLines() + .filter(_.head == '<') + .map(_.replaceAll("[<> ]*", "")) + .map(_.split("\\|\\|").toSeq.sorted) + .toArray.sortBy(_.mkString("||")) + source.close() + res + } +} + diff --git a/src/main/scala/views/interference/operators/Transform.scala b/src/main/scala/views/interference/operators/Transform.scala new file mode 100644 index 0000000..074eb3e --- /dev/null +++ b/src/main/scala/views/interference/operators/Transform.scala @@ -0,0 +1,76 @@ +package views.interference.operators + +import pml.model.configuration.TransactionLibrary +import pml.model.configuration.TransactionLibrary.{UserScenarioId, UserTransactionId} +import pml.model.hardware.Platform +import pml.model.software.Application +import views.interference.model.specification.InterferenceSpecification.{PhysicalTransaction, PhysicalTransactionId} + +private[operators] trait Transform[L,R]{ + def apply(l:L):R +} + +object Transform { + + trait BasicInstances { + self:Platform => + + /** + * Convert a physical id to the corresponding path of services + * @group transform_operator + */ + given Transform[PhysicalTransactionId,Option[PhysicalTransaction]] with { + def apply(l: PhysicalTransactionId): Option[PhysicalTransaction] = + transactionsByName.get(l) + } + + /** + * Convert an application to the set of transaction id its trigger + * @group transform_operator + */ + given Transform[Application,Set[PhysicalTransactionId]] with { + def apply(l: Application): Set[PhysicalTransactionId] = + transactionsBySW.getOrElse(l, Set.empty) + } + } + + trait TransactionLibraryInstances { + self:TransactionLibrary & Platform => + + /** + * Convert a user transaction to its physical transaction id + * @group transform_operator + */ + given Transform[Transaction,Option[PhysicalTransactionId]] with { + def apply(l: Transaction): Option[PhysicalTransactionId] = + transactionByUserName.get(l.userName) + } + + /** + * Convert a user defined scenario to the set of its physical scenario ids + * @group transform_operator + */ + given Transform[Scenario,Set[PhysicalTransactionId]] with { + def apply(l: Scenario): Set[PhysicalTransactionId] = + scenarioByUserName.getOrElse(l.userName, Set.empty) + } + + /** + * Convert a user transaction id to its physical transaction id + * @group transform_operator + */ + given Transform[UserTransactionId,Option[PhysicalTransactionId]] with { + def apply(l: UserTransactionId): Option[PhysicalTransactionId] = + transactionByUserName.get(l) + } + + /** + * Convert a user scenario id to the set of its physical scenario ids + * @group transform_operator + */ + given Transform[UserScenarioId,Set[PhysicalTransactionId]] with { + def apply(l: UserScenarioId): Set[PhysicalTransactionId] = + scenarioByUserName.getOrElse(l, Set.empty) + } + } +} diff --git a/src/main/scala/views/interference/operators/Transparent.scala b/src/main/scala/views/interference/operators/Transparent.scala new file mode 100644 index 0000000..0f6a2a2 --- /dev/null +++ b/src/main/scala/views/interference/operators/Transparent.scala @@ -0,0 +1,41 @@ +package views.interference.operators + +import views.interference.model.relations.TransparentSet +import views.interference.model.specification.InterferenceSpecification.PhysicalTransactionId + +private[operators] trait Transparent[T] { + def apply(x:T):Unit +} + +object Transparent{ + + /** + * If an element x of type T is transparent then the operator can be used as follows + * {{{x.isTransparent}}} + */ + trait Ops { + extension[T](x:T){ + /** + * The element x is discarded for interference analysis + * @param ev proof that T can be discarded + */ + def isTransparent(using ev:Transparent[T]): Unit = ev(x) + } + } + + given [T](using ev:TransparentSet[T]):Transparent[T] with { + def apply(x:T): Unit = ev.value += x + } + + given [U] (using transformation: Transform[U,Option[PhysicalTransactionId]], + ev:Transparent[PhysicalTransactionId]): Transparent[U] with { + def apply(x:U): Unit = for {id <- transformation(x)} ev(id) + } + + + given [V] (using transformation: Transform[V,Set[PhysicalTransactionId]], + ev:Transparent[PhysicalTransactionId]): Transparent[V] with { + def apply(x: V): Unit = for {id <- transformation(x)} ev(id) + } + +} diff --git a/src/main/scala/views/interference/operators/package.scala b/src/main/scala/views/interference/operators/package.scala new file mode 100644 index 0000000..4fac866 --- /dev/null +++ b/src/main/scala/views/interference/operators/package.scala @@ -0,0 +1,23 @@ +package views.interference + +/** + * Package containing the operators related to interference computation that can be used on a PML model. + * Examples are provided in [[views.interference.examples.simpleKeystone.SimpleKeystoneInterferenceGeneration]] + * @note This package should be imported in all pml models as so + * {{{import views.interference.operators._}}} + * @see [[Analyse.Ops]] provides the operators related to interference computation with Monosat [[https://github.com/sambayless/monosat]] + * @see [[PostProcess.Ops]] provides the operators related to the post processing of the interference computation + * @see [[Interfere.Ops]] provides the operators related to interference assumptions + * @see [[Exclusive.Ops]] provides the operators related to exclusive assumptions (e.g., two [[pml.model.software.Application]] + * will not execute simultaneously) + * @see [[Transparent.Ops]] proves the operators related to transparency assumptions (e.g., a [[pml.model.configuration.TransactionLibrary.Transaction]] + * is discarded) + * + * + */ +package object operators extends Analyse.Ops + with PostProcess.Ops + with Interfere.Ops + with Exclusive.Ops + with Transparent.Ops + with Equivalent.Ops \ No newline at end of file diff --git a/src/main/scala/views/interference/package.scala b/src/main/scala/views/interference/package.scala new file mode 100644 index 0000000..368e4d6 --- /dev/null +++ b/src/main/scala/views/interference/package.scala @@ -0,0 +1,38 @@ +package views + +/** + * Package containing all interference modelling and analysis features of PML + * + * @see [[views.interference.model]] for basic classes used to model the interference analysis assumption + * @see [[views.interference.operators]] for operators used to manipulate and perform analyses + * @see [[views.interference.exporters]] for exporters + * @see [[views.interference.examples]] for examples on operator and class usage and + * the related documentation in src/main/doc-resources/pml/examples + * @groupname transform_operator Transform operators + * @groupprio transform_operator 4 + * @groupname service_relation Service relations + * @groupprio service_relation 3 + * @groupname transaction_relation Physical transaction relations + * @groupprio transaction_relation 3 + * @groupname scenario_relation Physical scenario relations + * @groupprio scenario_relation 3 + * @groupname equivalence_relation Equivalence relations + * @groupprio equivalence_relation 3 + * @groupname utilFun Utility functions + * @groupprio utilFun 5 + * @groupname exclusive_relation Exclusive relations + * @groupprio exclusive_relation 3 + * @groupname interfere_relation Interfere relations + * @groupprio interfere_relation 3 + * @groupname transparent_relation Transparent relations + * @groupprio transparent_relation 3 + * @groupname exclusive_predicate Exclusive predicates + * @groupprio exclusive_predicate 2 + * @groupname interfere_predicate Interfere predicates + * @groupprio interfere_predicate 2 + * @groupname equivalence_predicate Equivalence predicates + * @groupprio equivalence_predicate 2 + * @groupname transparent_predicate Transparent predicates + * @groupprio transparent_predicate 2 + */ +package object interference diff --git a/src/main/scala/views/package.scala b/src/main/scala/views/package.scala new file mode 100644 index 0000000..15700c1 --- /dev/null +++ b/src/main/scala/views/package.scala @@ -0,0 +1,7 @@ + +/** + * Package containing the modelling and analysis features for specific purposes + * @see [[views.interference]] for interference modelling and analysis + * @see [[views.dependability]] for dependability modelling and analysis + */ +package object views \ No newline at end of file diff --git a/src/main/scala/views/patterns/examples/PhylogPatterns.scala b/src/main/scala/views/patterns/examples/PhylogPatterns.scala new file mode 100644 index 0000000..843858e --- /dev/null +++ b/src/main/scala/views/patterns/examples/PhylogPatterns.scala @@ -0,0 +1,224 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.patterns.examples + +import views.patterns.exporters.LatexCodePrinter._ +import views.patterns.exporters.LatexDiagramPrinter._ +import views.patterns.model.DSLImplicits._ + +import scala.sys.process.Process + +object PhylogPatterns extends App { + + val patternWidth = 930 + + val alterationIdentified = + conclusion("All critical and innocuous configuration settings alterations $\\mathcal{A}lt$ identified (no case missing)"). + strategy("Safety Analysis" backing "Architecture mastery"). + evidence("Completeness: former alterations data-base"). + `given`("Safety objectives $\\mathcal{R}eq$ identified (ARP4754)"). + `given`("Configuration settings $\\mathcal{C}s$"). + `given`("Application mapping") + + val alterationMitigated = + conclusion ("Design of adequate mitigation means against inadvertent changes to Configuration Settings" size 8). + strategy("Check all identified settings alterations are mitigated ($\\forall a_i \\in \\mathcal{A}lt$ $a_i$ mitigated)"). + evidence(alterationIdentified). + evidence("$a_i$ is mitigated") + + val RU2 = + conclusion ( + "Designed, implemented and verified adequate mitigation means against " + + "inadvertent critical configuration setting alterations" + short "RU2" + size 8 + width patternWidth). + strategy ("DO178: Implementation compliant with specification \\\\ DO254: Hardware usage compliant with specification" size 11 ). + evidence (alterationMitigated). + `given` ("Mitigation means implementation verified \\\\ (DO178B/C compliance)"). + `given`("Hardware configuration verified \\\\ (DO254 compliance)") + + + RU2.printDiagram() + RU2.printCode() + + val platformAnalysis = + conclusion("Safety objectives $\\mathcal{R}eq$ are fulfilled"). + strategy("Safety Analysis at platform level"). + evidence("Real-time objectives are fulfilled"). + `given`("Safety objectives $\\mathcal{R}eq$,\\\\ configuration settings \\\\and application mapping"). + `given`(s"Mitigation means of RU2, RU3 and (E7)"). + `given`(s"Critical alteration from RU2") + + val equipmentAnalysis = conclusion("Errors contained within equipment" short "equipmentAnalysis" size 8). + strategy("Safety analysis at equipment level"). + evidence(platformAnalysis). + evidence("Design of adequate safety net") + + val failure_mode_identified = + conclusion("All failures and all theirs safety effects are identified"). + strategy("Safety Analysis " backing "Architecture mastery"). + evidence("Completeness: former accident data-base (e.g errata)"). + `given`("Safety objectives $\\mathcal{R}eq$ identified (ARP4754)"). + `given`("Configuration settings $\\mathcal{C}s$"). + `given`("Application mapping") + + val failure_mode_mitigated = + conclusion("Design of adequate mitigation means"). + strategy("Traceability analysis: \\\\ Check all identified failure modes are mitigated" size 6). + evidence(failure_mode_identified). + evidence("$fm_i$ is mitigated") + + val implementationCompliant = conclusion("Designed, implemented and verified adequate mitigation means for identified failure modes" short "implementationCompliant" size 8). + strategy("DO178: Implementation compliant with specification \\\\ DO254: Hardware usage compliant with specification" size 8). + evidence(failure_mode_mitigated). + `given`("Mitigation means implementation verified \\\\ (DO178B/C compliance)"). + `given`("Hardware configuration verified \\\\ (DO254 compliance)") + + + val EH = + conclusion("Mitigation means commensurate with the safety objectives" short "EH" width patternWidth size 8). + strategy("Check matching between equipment safety analysis and mitigation means implementation"). + evidence(implementationCompliant). + evidence(equipmentAnalysis) + + EH.printDiagram() + EH.printCode() + + + //TODO short must be derived from variable name and purify from LaTeX forbidden characters (_,$) + val EHSplit = conclusion("Mitigation means commensurate with the safety objectives" short "EHSplit" width patternWidth size 8). + strategy("Check matching between equipment safety analysis and mitigation means implementation"). + evidenceRef(implementationCompliant). + evidenceRef(equipmentAnalysis) + + EHSplit.printDiagram() + EHSplit.printCode() + + + val interference_identification = + conclusion("Identification of all interference $\\mathcal{I}$"). + strategy("Interference calculus"). + `given`("Configuration settings $\\mathcal{C}s$"). + `given`("Application mapping") + + val interference_identified_classified = + conclusion("Classification of interference effects $( \\forall i \\in \\mathcal{I}, c(i))$"). + strategy("Safety analysis" backing "Architecture mastery"). + evidence(interference_identification). + evidence("Identification of $i$ effects"). + `given`("Configuration settings $\\mathcal{C}s$ and application mapping and temporal constraints on applications (e.g. WCET)") + + val identified_mitigated = + conclusion("Design of adequate means of mitigation for interference"). + strategy("Check all identified interference are mitigated ($\\forall i \\in \\mathcal{I}$, $i$ mitigated)"). + evidence(interference_identified_classified). + evidence("$i$ mitigated\\\\(e.g. prevention / blocking with run-time mechanism; impossible due to usage domain restriction; tolerance)") + + val RU3 = + conclusion("Identification of interference and verified means of mitigation" short "RU3" width patternWidth). + strategy("DO178: Implementation compliant with specification \\\\ DO254: Hardware usage compliant with specification" size 11 ). + evidence(identified_mitigated). + `given`("Mitigation means implementation verified \\\\ (DO178B/C compliance)"). + `given`("Hardware configuration verified \\\\ (DO254 compliance)") + + + RU3.printDiagram() + RU3.printCode() + + val interference_identified_classified_short = + conclusion("Classification of interference effects $( \\forall i \\in \\mathcal{I}, c(i))$"). + strategy("Safety analysis" backing "Architecture mastery"). + evidence("Identification of all interference $\\mathcal{I}$ \\\\ Given: Configuration settings $\\mathcal{C}s$"). + evidence("Identification of $i$ effects"). + `given`("Configuration settings $\\mathcal{C}s$ and application mapping and temporal constraints on applications (e.g. WCET)") + + val identified_mitigated_short = + conclusion("Design of adequate means of mitigation for interference"). + strategy("Check all identified interference are mitigated ($\\forall i \\in \\mathcal{I}$, $i$ mitigated)"). + evidence(interference_identified_classified_short). + evidence("$i$ mitigated\\\\(e.g. prevention / blocking with run-time mechanism; impossible due to usage domain restriction; tolerance") + + val RU3_short = + conclusion("Identification of interference and verified means of mitigation" short "RU3Short" width patternWidth). + strategy("DO178: Implementation compliant with specification \\\\ DO254: Hardware usage compliant with specification" size 11 ). + evidence(identified_mitigated_short). + `given`("Mitigation means implementation verified \\\\ (DO178B/C compliance)"). + `given`("Hardware configuration verified \\\\ (DO254 compliance)") + + + RU3_short.printDiagram() + RU3_short.printCode() + + val software_demand_identified = + conclusion("Identify demand of all software"). + strategy("Formal methods and tests"). + `given`("- Execution model \\\\- Final configuration settings \\\\- Software implementation"). + evidence("Definition of \\emph{software demand}") + + val hardware_capacity_identified = + conclusion("HW resources with their capacities identified"). + strategy("Reading and stressing benchmarks" backing "architecture mastery"). + `given`("MCP resources"). + `given`("Configuration settings $\\mathcal{C}s$ and application mapping"). + evidence("Synthesis of HW documentation"). + evidence("Definition of \\emph{amount of resources available}") + + val RU4 = + conclusion("Identify the available MCP resources, how they are allocated and verify demand does not exceed the amount of resources available " + short "RU4" + size 10 + width patternWidth). + strategy("Check demand on resources is correct"). + evidence(hardware_capacity_identified). + evidence(software_demand_identified.label) + + RU4.printDiagram() + RU4.printCode() + + val configuration_mitigation = conclusion("Configuration settings $i$ contributes to at least one requirement"). + strategy("Check $i$ contributes to at least one requirement"). + `given`("List of CAST32A objectives"). + evidence("List of PHYLOG diagrams to which $i$ contributes"). + evidence("Rationale of the contribution of $i$ to each PHYLOG diagrams") + + val correct_config_spec = + conclusion("Configuration settings specification enables the hardware and the software to satisfy the requirements"). + strategy("Check contribution of each configuration settings $(\\forall i \\in \\mathcal{C}s)$ to the requirements"). + evidence("Description of the configuration settings $\\mathcal{C}s$"). + evidence(configuration_mitigation). + evidence("Configuration settings $i$ not contributing to requirements is innocuous") + + val RU1 = conclusion( + "MCP configuration settings enable the hardware and the software hosted on the MCP to satisfy the functional, timing and safety requirements" + short "RU1" + size 10 + width patternWidth ). + strategy("DO178: Implementation compliant with specification \\\\ DO254: Hardware usage compliant with specification" size 11 ). + evidence(correct_config_spec). + `given`("Configuration settings means implementation verified \\\\ (DO178B/C compliance)"). + `given`("Hardware configuration verified \\\\ (DO254 compliance)") + + RU1.printDiagram() + RU1.printCode() + + System.getProperty("os.name").toLowerCase match { + case "linux" | "mac" => Process("make patterns").! + case _ => + } +} \ No newline at end of file diff --git a/src/main/scala/views/patterns/exporters/LatexCodePrinter.scala b/src/main/scala/views/patterns/exporters/LatexCodePrinter.scala new file mode 100644 index 0000000..4ffc40f --- /dev/null +++ b/src/main/scala/views/patterns/exporters/LatexCodePrinter.scala @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.patterns.exporters + +import pml.exporters.FileManager +import views.patterns.model._ + +import java.io.{File, FileWriter} + +trait LatexCodePrinter + +object LatexCodePrinter{ + implicit class LatexCodePrinterConclusion(conclusion: Claim) { + + private val ids = Claim.computeIdIn(conclusion) + + def printCode(): File = { + val file = { + for(s <- conclusion.short) yield + FileManager.exportDirectory.getFile(s"${s}_text.tex") + } getOrElse new File(s"${conclusion.label}_text.tex") + val writer = new FileWriter(file) + print(conclusion)(writer,0) + writer.close() + file + } + + private def print(s:String)(implicit printer: FileWriter, spacing:Int):Unit = { + addSpacing + s.split("""\\\\""").filterNot(_.isEmpty).toList match { + case h :: Nil => + printer.write(s"\\textrm{${h.trim}}\\\\\n") + case h :: t => + printer.write(s"\\textrm{$h}\\\\\n") + t.foreach ( e => { + addSpacing + printer.write(s"\\textrm{${e.trim}}\\\\\n") + }) + case _ => + } + } + + private def addSpacing(implicit printer: FileWriter, spacing:Int):Unit = + { + for(_ <- 1 to spacing) printer.write("\t") + if (spacing != 0) printer.write(s"\\hspace*{${5*spacing}pt}") + } + + private def print(conclusion: Claim)(implicit printer: FileWriter, spacing:Int): Unit = { + addSpacing + printer.write(s"\\texttt{Claim:} \\\\\n") + print(s"${ids(conclusion)} ${conclusion.label}")(printer,spacing + 1) + print(conclusion.strategy) + print(conclusion.evidences.toList) + conclusion.evidences foreach { + case c:Claim => + printer.write("\n") + print(c) + case _ => + } + } + + private def print(strategy: Strategy)(implicit printer: FileWriter, spacing:Int): Unit = { + addSpacing + printer.write(s"\\texttt{Strategy:} \\\\\n") + print(s"${ids(strategy)} ${strategy.label}")(printer,spacing + 1) + for (b <- strategy.backing) yield print(b)(printer,spacing + 1) + for (b <- strategy.defeater) yield print(b)(printer,spacing + 1) + } + + private def print(backing: Backing)(implicit printer: FileWriter, spacing:Int): Unit = { + addSpacing + printer.write(s"\\texttt{Backing:} \\\\\n") + print(backing.label)(printer,spacing + 1) + } + + private def print(defeater: Defeater)(implicit printer: FileWriter, spacing:Int): Unit = { + addSpacing + printer.write(s"\\texttt{Defeaters:} \\\\\n") + print(defeater.label)(printer,spacing + 1) + } + + private def print(evidences: List[Evidence])(implicit printer: FileWriter, spacing:Int): Unit = { + evidences collect { case g:Given => g} match { + case Nil => + case l => + addSpacing + printer.write(s"\\texttt{Givens}:\\\\\n") + l foreach( g => + print(s"${ids(g)} ${g.label}")(printer,spacing +1 ) + ) + } + evidences collect {case f:FinalEvidence => f; case c:Claim => c} match { + case Nil => + case l => + addSpacing + printer.write(s"\\texttt{Evidences}:\\\\\n") + l foreach(e => { + print(s"${ids(e)} ${e.label}")(printer,spacing +1) + }) + } + } + } +} diff --git a/src/main/scala/views/patterns/exporters/LatexDiagramPrinter.scala b/src/main/scala/views/patterns/exporters/LatexDiagramPrinter.scala new file mode 100644 index 0000000..4b543a7 --- /dev/null +++ b/src/main/scala/views/patterns/exporters/LatexDiagramPrinter.scala @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.patterns.exporters + +import pml.exporters.FileManager +import views.patterns.model._ + +import java.io.{File, FileWriter} +import java.nio.file.Paths + +object LatexDiagramPrinter { + implicit class LatexDiagramPrinterConclusion(conclusion: Claim) { + + private val ids = Claim.computeIdIn(conclusion) + + val file: File = { + for(s <- conclusion.short) yield + FileManager.exportDirectory.getFile(s"${s}_diagram.tex") + } getOrElse new File(s"${conclusion.label}_diagram.tex") + + private def getSize(textWidth:Option[Int]): String = + (for(s <- textWidth) yield s"set width = $s cm,") getOrElse "" + + + private def getImplementation(implementation:Option[String]) : String = + (for (i <- implementation) yield i) getOrElse "" + + private def writeString(s:String)(implicit printer: FileWriter, spacing:Int) : Unit ={ + s.split("\n").foreach { chunk => + addSpacing + printer.write(chunk + "\n") + } + } + + def printDiagram(): File = { + val writer = new FileWriter(file) + writer.write( + s""" + |\\ifdefined\\standalone + |\\documentclass{standalone} + |\\usepackage[utf8]{inputenc} + |\\usepackage{tikz} + |\\usetikzlibrary{shapes,arrows,backgrounds,positioning,calc, automata, shadows, backgrounds,fit, arrows,graphs, trees, decorations.pathreplacing, patterns} + |\\usepackage{forest} + |\\usepackage{hyperref} + |\\usepackage{varwidth} + |\\input{tikzStyle} + | + |\\begin{document} + """.stripMargin) + for(width <- conclusion.width) yield { + writer.write(s"\\resizebox{$width px}{!}{") + } + writer.write("\\fi") + writer.write( "\\begin{forest}\n\tfor tree = { edge = {latex-}, parent anchor=south, child anchor=north, l sep+=.5em, s sep+= 1em },\n") + print(conclusion)(writer,1) + writer.write("""\end{forest}""") + writer.write("\\ifdefined\\standalone") + for(_ <- conclusion.width) yield { + writer.write("}") + } + writer.write("\\end{document}\n\\fi") + writer.close() + file + } + + private def addSpacing(implicit printer: FileWriter, spacing:Int):Unit = + (1 to spacing) foreach (_ => printer.write("\t")) + + private def print(claim: Claim)(implicit printer: FileWriter, spacing:Int): Unit = { + writeString(s"[{${ids(claim)} ${claim.label + getImplementation(claim.implementation)}}, conclusion, s sep-= 1.25em, calign=first, ${getSize(claim.textWidth)} \n") + for (b <- claim.strategy.defeater) yield print(b)(printer,spacing + 1) + print(claim.strategy)(printer, spacing + 1) + claim.evidences foreach (e => print(e)(printer, spacing + 2)) + writeString("]\n")(printer, spacing + 1) + for (b <- claim.strategy.backing) yield print(b)(printer,spacing + 1) + writeString("]\n") + } + + private def print(strategy: Strategy)(implicit printer: FileWriter, spacing:Int): Unit = { + writeString(s"[{${ids(strategy)} ${strategy.label} ${getImplementation(strategy.implementation)}}, strategy, ${getSize(strategy.textWidth)}\n") + } + + private def print(backing: Backing)(implicit printer: FileWriter, spacing:Int): Unit = { + writeString(s"[{${backing.label}}, backing, no edge]\n") + } + + private def print(defeater: Defeater)(implicit printer: FileWriter, spacing:Int): Unit = { + writeString(s"[{${defeater.label}}, defeater, no edge]\n") + } + + private def print(evidence: Evidence)(implicit printer: FileWriter, spacing:Int): Unit = evidence match { + case e@FinalEvidence(content, implementation, textWidth, None) => + writeString(s"[{${ids(e)} $content ${getImplementation(implementation)}}, conclusion, ${getSize(textWidth)}] \n") + case e@FinalEvidence(content, implementation, textWidth, Some(refOf)) => + val refPrinter = LatexDiagramPrinterConclusion(refOf) + val refPath = Paths.get(refPrinter.file.getName).toString.replace(".tex",".pdf") + refPrinter.printDiagram() + writeString(s"[{\\href{$refPath}{${ids(e)}} $content ${getImplementation(implementation)}}, conclusion, ${getSize(textWidth)}] \n") + case g@Given(content, implementation, textWidth) => + writeString(s"[{${ids(g)} $content ${getImplementation(implementation)}}, conclusion, ${getSize(textWidth)}, dashed ] \n") + case c: Claim => + print(c) + } + } +} + +trait LatexDiagramPrinter \ No newline at end of file diff --git a/src/main/scala/views/patterns/exporters/allPrinters.scala b/src/main/scala/views/patterns/exporters/allPrinters.scala new file mode 100644 index 0000000..199c86a --- /dev/null +++ b/src/main/scala/views/patterns/exporters/allPrinters.scala @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.patterns.exporters + +object allPrinters extends LatexCodePrinter with LatexDiagramPrinter diff --git a/src/main/scala/views/patterns/model/PatternAST.scala b/src/main/scala/views/patterns/model/PatternAST.scala new file mode 100644 index 0000000..fb969bc --- /dev/null +++ b/src/main/scala/views/patterns/model/PatternAST.scala @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2021. ONERA + * This file is part of PML Analyzer + * + * PML Analyzer is free software ; + * you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation ; + * either version 2 of the License, or (at your option) any later version. + * + * PML Analyzer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY ; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this program ; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + ******************************************************************************/ + +package views.patterns.model + +import scala.language.implicitConversions + +trait PatternAST { + val label:String + val implementation:Option[String] + val textWidth:Option[Int] +} + +sealed trait Evidence extends PatternAST + +case class Backing(label:String, implementation:Option[String] = None, textWidth:Option[Int] = None) extends PatternAST + +case class Defeater(label:String, implementation:Option[String] = None, textWidth:Option[Int] = None) extends PatternAST + +case class Strategy(label:String, backing: Option[Backing] = None, defeater: Option[Defeater]=None, implementation:Option[String] = None, textWidth:Option[Int] = None) extends PatternAST{ + def backing(s:String) : Strategy = + copy(backing = Some(Backing(s))) + def defeater(s:String): Strategy = + copy(defeater = Some(Defeater(s))) +} + +case class FinalEvidence(label:String, implementation:Option[String] = None, textWidth: Option[Int] = None, refOf:Option[Claim]=None) extends Evidence + +case class Given(label:String, implementation:Option[String] = None, textWidth: Option[Int] = None) extends Evidence + +case class Claim(label:String, short:Option[String], implementation:Option[String], textWidth:Option[Int], width:Option[Int], strategy: Strategy, evidences: Evidence*) extends Evidence { + def evidence (s:String) : Claim = + Claim(label, short, implementation, textWidth, width, strategy, evidences :+ FinalEvidence(s):_*) + def evidence (b:Builder) : Claim = + Claim(label, short, implementation, textWidth, width, strategy, evidences :+ FinalEvidence(b.content, b.implementation, b.textWidth):_*) + def `given` (s:String) : Claim = + Claim(label, short, implementation, textWidth, width, strategy, evidences :+ Given(s):_*) + def `given` (b:Builder) : Claim = + Claim(label, short, implementation, textWidth, width, strategy, evidences :+ Given(b.content,b.implementation, b.textWidth):_*) + def evidence (e:Evidence) : Claim = + Claim(label, short, implementation, textWidth, width, strategy, evidences :+ e:_*) + def evidenceRef(claim:Claim) : Claim = + Claim(label, short, implementation, textWidth, width, strategy, evidences :+ FinalEvidence(claim.label,refOf = Some(claim)) :_*) + def allRef: Set[Claim] = evidences.collect { + case c:Claim => c.allRef + case FinalEvidence(_,_,_,Some(ref)) => Set(ref) + }.toSet.flatten +} + +object Claim{ + + def computeIdIn(c:Claim):Map[PatternAST,String] = { + + val strategyIdByLevel = collection.mutable.HashMap.empty[Int,Int].withDefaultValue(0) + val evidenceIdByLevel = collection.mutable.HashMap.empty[Int,Int].withDefaultValue(0) + val givenIdByLevel = collection.mutable.HashMap.empty[Int,Int].withDefaultValue(0) + + def getFreshId(level:Int, c:collection.mutable.Map[Int,Int]) : () => Int = { + c(level) = c(level) + 1 + val x = c(level) + () => (0 until level).map(i => c(i)).sum + x + } + + def byLevel(c: Claim, currentLevel: Int) : Map[PatternAST, () => String] = { + val cId = (for {s <- c.short if currentLevel == 0} yield () => s"($s)") getOrElse { + val x = getFreshId(currentLevel,evidenceIdByLevel) + () => s"(E${x()})" + } + val sId = { + val x = getFreshId(currentLevel,strategyIdByLevel) + () => s"(W${x()})" + } + c.evidences.flatMap{ + case f:FinalEvidence => + val x = getFreshId(currentLevel + 1,evidenceIdByLevel ) + Set(f -> (() => s"(E${x()})")) + case g:Given => + val x = getFreshId(currentLevel + 1,givenIdByLevel ) + Set(g -> (() => s"(G${x()})")) + case c:Claim => + byLevel(c,currentLevel + 1) + }.toMap + (c -> cId) + (c.strategy -> sId) + } + byLevel(c,0).transform((_,v) => v()) + } +} + +case class Builder(content:String, short:Option[String], implementation:Option[String], textWidth:Option[Int], width:Option[Int]) { + def short(s:String) : Builder = + Builder(content,Some(s),implementation,textWidth, width) + def size(s:Int) : Builder = + Builder(content,short,implementation,Some(s), width) + def width (s:Int) : Builder = + Builder(content,short,implementation,textWidth, Some(s)) + def strategy (s:Strategy): Claim = + Claim(content,short,implementation, textWidth, width, s) + def strategy (b:Builder): Claim = + Claim(content,short,implementation, textWidth, width, Strategy(b.content,None,None,implementation,b.textWidth)) + def backing (s:String) : Strategy = + Strategy(content,Some(Backing(s)),None,implementation,textWidth) + def defeater (s:String) : Strategy = + Strategy(content,None,Some(Defeater(s)),implementation,textWidth) + def implementation (i:String): Builder = + copy(implementation = Some(s"\\textcolor{red}{$i}")) +} + +object DSLImplicits { + + implicit def toBuilder(s:String) : Builder = + Builder(s,None,None,None,None) + + def conclusion (s:Builder) : Builder = s + +} \ No newline at end of file diff --git a/tikzStyle.tex b/tikzStyle.tex new file mode 100644 index 0000000..4d53e75 --- /dev/null +++ b/tikzStyle.tex @@ -0,0 +1,39 @@ +\tikzset{block/.style={draw,rectangle,text centered, text width=#1}} +\tikzset{block/.default={draw,rectangle}} +\tikzset{arc/.style={-latex}} +\tikzset{back/.style={latex-}} +\tikzset{connect/.style={draw, circle, fill=black, scale=.3}} +\tikzset{br/.style n args={2}{ below=#1 of #2, anchor= south west}} +\tikzset{bl/.style n args={2}{ below=#1 of #2, anchor= south east}} +\tikzset{ar/.style n args={2}{ above=#1 of #2.south east}} +\tikzset{al/.style n args={2}{ above=#1 of #2.south west}} +\tikzset{bolded/.style={line width= 2 pt}} + +\tikzset{com/.style={draw, ellipse, thick, text centered, text width=3cm}} + + +\tikzset{ + max width/.style args={#1}{ + execute at begin node={\begin{varwidth}{#1}}, + execute at end node={\end{varwidth}} + } +} +\tikzset{ + set width/.style args={#1}{ + text width=#1, + execute at begin node={\begin{varwidth}{#1}}, + execute at end node={\end{varwidth}} + } +} + +\tikzset{conclusion/.style={draw=blue, rectangle, rounded corners, thick, max width=4cm}} +\tikzset{evidences/.style={draw=blue, rectangle, rounded corners, fill=blue!20, text color= white, thick, text centered}} +\tikzset{strategy/.style={trapezium,trapezium left angle=60, trapezium right angle=120,draw, text centered}} +\tikzset{softStrategy/.style={trapezium,draw, fill=yellow!20, text centered}} +\tikzset{backing/.style={draw, trapezium, trapezium left angle=60, trapezium right angle = 90}} +\tikzset{defeater/.style={draw=red!80, trapezium, trapezium left angle=90, trapezium right angle = 120}} + +\newcommand{\eventFigAbove}[1]{\hspace{1cm} #1\\ \vspace{.1cm}\Huge\Lightning} +\newcommand{\eventFigBelow}[1]{{\Huge\rotatebox{180}{\Lightning}}\\ \vspace{.1cm} #1} +\newcommand{\eventFigRight}[1]{ {\Huge\rotatebox{270}\Lightning} \vspace{.1cm} #1} +\newcommand{\eventFigLeft}[1]{ #1 \vspace{.1cm} {\Huge\rotatebox{90}\Lightning}}
  • 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�~%TaO6*-%O!v!Jm|UfV28 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 CK}zue literal 0 HcmV?d00001 diff --git a/src/main/doc-resources/pml/examples/simpleT1042/SimpleDoc.md b/src/main/doc-resources/pml/examples/simpleT1042/SimpleDoc.md new file mode 100644 index 0000000..60e5f04 --- /dev/null +++ b/src/main/doc-resources/pml/examples/simpleT1042/SimpleDoc.md @@ -0,0 +1,104 @@ +# Documentation for Simple example + +## Platform + +This processor is composed of: +* two cores, +* one DMA (Direct Access Memory), +* one Ethernet device, +* two memory controllers (the memories behind the memory controllers are not represented), +* one PCIe controller, +* an interrupt controller (MPCI), +* and a set of configuration registers reachable through a specific configuration bus. + +All the resources necessary for executing program instructions are locally hosted by each core: ordinal counter, registers, computing units, etc. +These resources are private to each core. +They can be used simultaneously without interference by each core. +Conversely, the memory hierarchy is composed of resources local to each core (the cache memories), and two global memories reachable from the two cores. +These global memories are shared resources. + +The cores, the Ethernet device, the DMA, and the memories are connected by a central bus as shown in Figure 1. +This bus carries transactions, that is, requests from the cores, the Ethernet port, and the DMA to the memories and to the configuration bus. +As such, the central bus is a shared resource that can be used asynchronously by the transactions. +These transactions can collide when attempting to cross the bus at the same time. + +![platform](platform.PNG "multicore processor") + +Figure 1: Multicore processor + +PML Encoding is provided in src/main/scala/pml/examples/simple/SimplePlatform.scala + +## Software Allocation + +The application layer is composed of five tasks: +* app4 is an asynchronous microcode running on the eth component. +* app21 is a periodic task running on core2. +* app22 is a periodic task running on core2. +* app3 a microcode running on DMA. +* app1 is an asynchronous applicative task running on core1. + +PML Encoding is provided in src/main/scala/pml/examples/simple/SimpleSoftwareAllocation.scala + +## Transaction library + +The application layer is composed of five tasks: +* app4 Each time an Ethernet frame arrives, it transfers the payload of the frame to mem2 (transaction t41). +* app21 is a periodic task running on core2. At each period app21 reads the last Ethernet message from mem2, makes some input treatments on it, and makes it available for app1 in mem1. +* app22 is a periodic task running on core2. Similarly, at each period app22 reads output data of app1 from mem1. It transforms them into PCIe frames. The frames are then store in mem2. +* app22 wakes up the DMA (app3) by writing the address of the PCIe frames into the DMA registers. +* app3 a microcode running on DMA. When woke up, app3 reads the PCIe frame from mem2 and transfers it to pcie. +* app1 is an asynchronous applicative task running on core1 and activated each time an external interrupt arrives. It begins by reading the interrupt code from mpic (transaction t11). It reads its input data from mem1 (transaction t12). Then it runs using the internal cache of core1(transaction t13). And finally it stores its output data in mem1(transaction t14). + +![transaction](transactions.PNG "Transactions of the HW architecture (the red, violet,blue, and green arrows represent respectively the transactions of app1,app2(app21 app22), app3,and app4)") + +Figure 2: Transactions of the HW architecture (the red, violet,blue, and green arrows represent respectively the transactions of app1,app2(app21 app22), app3,and app4). + +PML Encoding is provided in src/main/scala/pml/examples/simple/SimpleTransactionLibrary.scala + +In this example all defined transaction are used, the configuration of the library is provided in src/main/scala/examples/simple/SimpleLibraryConfiguration + +## Routing + +In this example there are not multiple paths between target and initiators so the routing configuration is optional (here empty) + +## Specifications + +In this example we consider that +* bus services are independent +* DMA and config_bus services impacts each others +* app21 and app22 are exclusive + +PML encoding is provided in src/main/scala/views/interference/examples/simple/SimpleTableBasedInterferenceSpecification + +## Exports + +### Configured platform + +The file src/main/scala/pml/examples/simple/SimpleExport shows how graphical exports are produced (stored in export folder) +from a platform: +* graph of used SW and HW +* graph of used services per application +* table of transaction +* table of data +* table of SW allocation to HW +* table of component activation +* table of SW usage +* routing table +* transfert table + +### Interference analysis + +The file src/main/scala/views/interference/examples/SimpleInterferenceGeneration shows how interference analysis can be performed +on a configured platform. The generated files are stored in analysis folder: +* computation of n-itf +* computation of n-free +* computation of n-channels + +As an example the following interference is identified as a 3-itf + +< app1_rd_d1 || app21_wr_d1 || app4_wr_input_d > + +![transaction](interference.PNG "Footprint and interference channel (identified by the two circles)") + +Figure 3: Footprint and interference channel (identified by the two circles) + diff --git a/src/main/doc-resources/pml/examples/simpleT1042/interference.PNG b/src/main/doc-resources/pml/examples/simpleT1042/interference.PNG new file mode 100644 index 0000000000000000000000000000000000000000..55626796efbbaabbefbcf5ecd476f782a16c0a95 GIT binary patch literal 37286 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 TS{VNq{H3g*A)haM*ZcnfGV|5D literal 0 HcmV?d00001 diff --git a/src/main/doc-resources/pml/examples/simpleT1042/platform.PNG b/src/main/doc-resources/pml/examples/simpleT1042/platform.PNG new file mode 100644 index 0000000000000000000000000000000000000000..57e7d203439cea290cd048d0d4f07432e80c3703 GIT binary patch literal 23007 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/andBlue.gif b/src/main/resources/icons/generic/andBlue.gif new file mode 100644 index 0000000000000000000000000000000000000000..555da22ae3d85afdb25ae28a149c56d4d85c1b19 GIT binary patch literal 599 zcmZ?wbhEHblwuHNC}%KK{3jKhpH@vk zCI&_Z2?imCHV}`2fuCQ1m6c6OT1G)pSy@F*TUX!6#M~mb%ObARJhsC+vcodA%N$6? z^;pGsTLMLrdaRRL?2{(iCUm=c_&B+GI;Qr!q)&E9pX8Y})i-}yK;g`wl38w9lU>p} z9FlsR(E_y(YJF zP3`pU?R`@xOrO`aXkXicU6WQEnz`=yiVZv0Zr#6f_r=YJZ|pmB@6x?D$FDxUdjHj- zi;phfeR=2Er|Xa2-F)9 zpFD4pwal!(`Aa8S%ks`yw#H6w^_pe&@(gn~F4f;UUyPB_a*eIhwux4(4r0b@T=)-9 z+#~L~v3Z^e6Pu3i$)%3UXPr+tvF}@Eu5!$XFD_X e-g`DbA2d22>lB%-AQ3fV0aJtMYzbB-25SHu53NK1 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/andGreen.gif b/src/main/resources/icons/generic/andGreen.gif new file mode 100644 index 0000000000000000000000000000000000000000..a38c4388702c15b773d2aba4b3116a302ff687cc GIT binary patch literal 394 zcmV;50d@XINk%w1VHyAx0CxZ}A^sXuZ)S9NVRB_UAWdmwa&L2QW^^D=W@c$)WdI@h z1OWg5001lk00ICQ01^O&00000000dS3IzoO7aI~BAr~Gb7$z+rEHooMNHIG_FIQ$p zVRBA)j9`D2W|XdXnY4SA&{LAoP?phFl+jg@&rp%kQJ2zJjLcD)(ps3)Ta(UFnbTdC z(pQMgRg=+GmC;m^&{37qR+Y|Em(N+1(N&evSCr6Hlh9C;(^jh5cA(W~rrB<*+IXVY zYN6I?sn~L^+vk zW(H;kaRwoVRuGSYL6Do9jfF){L_}3WLPJW*NKw&BO)bF0#Mj8E$lJRXOMA3Uez{M9y-$F= zT!XMqfV@wGyjF+6f1=Hwr>~o%tD2yxnV_khqN|^#uaB(ItFyePuD7MEwWO=GqpGu_ zsE1%e%nGyTs1E#?igT(YeCRx4_D`z{$A4%Eiml!^qCk)!Ne5+SAtC z!OGRj)85F@+{e({#?RTs&e+4u*TBit%hcc7-r>;M<;>RM)!*vW-s#ib>CxNg(Aeb7 z*W%38;mg(F&)DSC-00lq^4jF^+vV}x+tpI?)2g5@#5+7`}_O)`uhF-{q^{{Hjz_~`2F=;`e4?(gjE?C|jL z@bdNl|Nj930|5d90RjU70s{d80|5d90RjU70s{d80|5d92mgQof`f#F0f&W&ii?Mh zh>MVej*^d&i~)&_mWq{yotuK6l$H+)rl+W>s-_Qx3RbYOva__X3a+)exwnL`y1%i! zgTBDO!h#D|%FD`F&d<8X01#kd)?#C0WMpM#W@lMiS6bxaS6B;$7j^9I?(Jx4YVUPm z0);5_`fF=!ZSQUd6k@PL#*7&}die11Bgq>$apcTN^Uoa}0)+~sz>q=1hL0RNc!(fj z0)+||K6F?KA_NTsg$w|pp<+b}7A;)7fFWW8iIpWlWXNFRM2s2-3Jp-QB#00pLxvD3 zVg!hd8#r?4xWV-XjUz%36cVt7OBW_+n>2Cavk z5Gl_f#?S`hF)-9@bgA3yI$?io;|}-My`IxIR!&}9HgR?7#8o8|R+jXyDDGQc)Vr*x z4~SM2O@A`ASYtMAAJ>9+LRM(o5T`P`tEIZt`G>h>E`wq2XF_3EU3_h)W7x?tafS^FQ(+W%nI zz6Uc8-CJ_v>aLU5cAdGg@6eMu2OiBn_;~h#hqKN--*oQ9<}=UM-+OoZ-upB6-(GzD z>GH!5m!7=3`|kVGci*1A`TFSBpYK0^{`~v*@4tWle*F6O>C>lQzkWS?{qXhc*WbT? z|Ns9#BNH>Ji2Z*9LsN503uAk0XIEEyZ%6lp*52m6i4!MxGESS`-9LNMte%C_ zj*gDG9T6E3<>MO~{v`aAVy8~t>o;%Tq@<>$zsrm34EXXhBQxvsyZBCyEd_;-j!e?v zSn9)gxPW)IS=x(I&yWL$8Cf`mGy*m>FgCOC%Ggve99(>oLx@F1=pz$bmx!E=&w_x1 zhgdl!j52O)U~Kf~lhxw!Fi~pjku=G;vLf*@Tc3o?@~ literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/ctClosedPurple.gif b/src/main/resources/icons/generic/ctClosedPurple.gif new file mode 100644 index 0000000000000000000000000000000000000000..11cf344652b758e7118a59ccb9ec482ddd5495ab GIT binary patch literal 749 zcmVZGck@dGmLph#Mi^Z)WE><+}rTl+40-j@7vn%+1TXW-R02F;mpkK z*w^gW*6Y^Q>ebZg)YIwH(&y38{`&Rx^78Wc_xJq#{P^?o?dj<3=;-k7?&#>~ z_3-ci|Nj930|5d90RjU70s{d80|5d90RjU70s{d80|5d92mgQof`f#D0RRDsg^P@f ziI0$tl7o+gm6MH^g@>4oik_UEmYs;jK6sSSk!Nwc)IwzsnavADdvx`eX5 z!nD7G!NbDEf&)s;&dp2F(!I(64`5;0Vq;@uWMyV%XG~2>P3PrGO9O=vb?@-;@Mvjj z@^xzjg(&y^Y;A7r#d}krkb@m5RH)$5!-tO_PT<6mGlxw*d2|dEIHqq2&hwj;}gcAQ7UV5P>LCrcha1rHU0RS1g`j$)aTol)F&4bn&vF(12*q f0yp^*1+A3BQVKU=M~HzdRs$kSo;-kopg;gS3w)Y2 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/ctClosedRed.gif b/src/main/resources/icons/generic/ctClosedRed.gif new file mode 100644 index 0000000000000000000000000000000000000000..e72376afc2844c2ef22910e4a39b138ebc77e980 GIT binary patch literal 735 zcmbV<=}(ez0Dyl;UUQi?XRfV6ZBx+?x>j2D!A?>8MRNLAgXgMZh~n z5X2*`nO9Sk7t~BiQJh=SF%U17OHc`umm~cbdiLzup4hnCQJ2&G!6{GxBK+;AALLihGI4pY4i}Y-#pYyY1OJ0#2yjT)QO^NiKs!1BAOb;($B*IhqeVqh0$~)7 zR|D_^0C{n-j6jeT7t2dZ#!E_+Jf4cdm?D#Bs8ltJwNzfd$Yd_i>GO2DvaD>DM*C7) zs-RM*DU=B!@e_$ON2C4ZaK2YnEwfl(D=Rfz?l-Y`Q6O0A>e5K1c6WCN0CoUY0N4V+ z41gX0D*z?{Y)GUPiCjgYU<_tIEp3lT)K*vTw6y$gYWmgKsBdW4tgm0Mt6QtBU9GLv z)zoOY+&w;jm(SmBZZ`6Gj*bpnd%L~89Ud8hB@%02A1oGI#bO%-!4RZ{pnV9kw6z_G zM1O_CeWB3Y+WJQzfZN(EA`z@oSr1hzHxvqkMq|`y*5>DJdcAFP)2h>1wze!5i_K=+ z)oM*96AZ(4yWLA4*FP{g)Q9fK!$FFbv@0pXV%AK>d15bPB$2qZb7JiH#K31lR;vmYjg-RaD7_d&<><|A&n9FL03 zr!TaU$b8SMcno{`mgkx1LZe*^}>x8$Iub!=6a*s}r&nK7Nd>fs8?6@5;ml7iT9B{SR8{ZHWK? literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/generic/equalBlue.gif b/src/main/resources/icons/generic/equalBlue.gif new file mode 100644 index 0000000000000000000000000000000000000000..70881e371165dffa9199d17d21aaeb23b55346d1 GIT binary patch literal 242 zcmVeJxw+~)Gv zvk z1_o9JNd_T?77&ktK~6-ZJ~;S41H(UF-oI>Ye_2`o@$mfR<^3lm^^cqTFB8*WZtj0< zZ2x(9{&R5r19BA=|5;l8FE0LHQSpEJ^#60`{%>mf-_h}Z`SS0J7X4qeh>?jIf>