Skip to content
Snippets Groups Projects
Commit 16734070 authored by René Schöne's avatar René Schöne
Browse files

Merge branch '33-create-pages-documentation' into 'dev'

Resolve "Create pages documentation"

See merge request !7
parents 33924f8d a185b579
No related branches found
No related tags found
2 merge requests!19dev to master,!7Resolve "Create pages documentation"
Pipeline #8797 passed
Showing
with 1445 additions and 58 deletions
......@@ -6,3 +6,4 @@ venv/
*/build/
logs/
datasets/
public/
......@@ -2,6 +2,9 @@ stages:
- build
- test
- report
- ragdoc_build
- ragdoc_view
- publish
variables:
# Instruct Testcontainers to use the daemon of DinD.
......@@ -17,33 +20,29 @@ variables:
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
cache:
paths:
- .gradle/wrapper
- .gradle/caches
build:
image: openjdk:8
tags:
- docker
stage: build
script:
- ./gradlew --console=plain --build-cache assemble
cache:
key: "$CI_COMMIT_REF_NAME"
policy: push
artifacts:
paths:
- build
- .gradle
- "eraser-base/src/gen"
test:
image: openjdk:8
tags:
- docker
stage: test
needs:
- build
script:
- ./gradlew --continue --console=plain --info check jacocoTestReport
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- build
- .gradle
artifacts:
when: always
paths:
......@@ -53,24 +52,66 @@ test:
coverage:
image: python:3.7.1-alpine
tags:
- docker
stage: report
dependencies:
needs:
- test
script:
# - ./gradlew --continue --console=plain -x test jacocoTestReport
- pip install --user untangle
- python print-coverage.py
coverage: "/Covered (\\d{1,3}\\.\\d{2}%) of instructions for all projects\\./"
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- build
- .gradle
allow_failure: true
artifacts:
when: always
paths:
- $JACOCO_REPORT
ragdoc_build:
image:
name: "git-st.inf.tu-dresden.de:4567/jastadd/ragdoc-builder"
entrypoint: [""]
stage: ragdoc_build
needs:
- build
script:
- JAVA_FILES=$(find eraser-base/src/ -name '*.java')
- /ragdoc-builder/start-builder.sh -excludeGenerated -d data/ $JAVA_FILES
artifacts:
paths:
- "data/"
ragdoc_view:
image:
name: "git-st.inf.tu-dresden.de:4567/jastadd/ragdoc-view:relations"
entrypoint: [""]
stage: ragdoc_view
needs:
- ragdoc_build
script:
- DATA_DIR=$(pwd -P)/data
- mkdir -p pages/docs/ragdoc
- OUTPUT_DIR=$(pwd -P)/pages/docs/ragdoc
- cd /ragdoc-view
- ( cd src/ && rm -rf data && ln -s $DATA_DIR )
- /ragdoc-view/build-view.sh --output-path=$OUTPUT_DIR
- ls -lah $OUTPUT_DIR
artifacts:
paths:
- "pages/docs/ragdoc"
pages:
image: python:3.8-buster
stage: publish
needs:
- test
- ragdoc_view
before_script:
- pip install -U mkdocs mkdocs-macros-plugin mkdocs-git-revision-date-plugin
script:
- cd pages && mkdocs build
only:
- dev
- master
artifacts:
paths:
- "public"
[submodule "ragdoc-view"]
path = ragdoc-view
url = ../../jastadd/ragdoc-view/
......@@ -129,35 +129,6 @@ sourceSets.main {
}
}
javadoc {
// this is only run to get the index file etc.
failOnError = false
}
String[] arguments = ["libs/rd-builder.jar", "-d", "doc/"]
def allSrcFiles = sourceSets.main.allSource.findAll { it.name.endsWith('java') }.toArray()
def ragdocViewSrcData = '../ragdoc-view/src/data/'
task ragdocFixed(type: JavaExec, dependsOn: assemble) {
group = 'documentation'
description = 'Create ragdoc json documentation files'
main = "-jar"
args arguments + allSrcFiles
}
task cleanRagdoc(type: Delete) {
group = 'documentation'
delete fileTree(ragdocViewSrcData + '/*')
}
task copyRagdoc(type: Copy, dependsOn: cleanRagdoc) {
group = 'documentation'
description = 'Copy ragdoc json documentation files to ragdoc-viewer'
from 'doc/'
into ragdocViewSrcData
eachFile { println it.file }
}
generateAst.dependsOn preprocess
generateAst.inputs.files file("./src/main/jastadd/mainGen.ast"), file("./src/main/jastadd/mainGen.jadd")
//compileJava.dependsOn jastadd
......
......@@ -5,6 +5,7 @@ import de.tudresden.inf.st.eraser.jastadd.model.*;
import de.tudresden.inf.st.eraser.util.ParserUtils;
import de.tudresden.inf.st.eraser.util.TestUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Disabled;
import java.io.IOException;
import java.util.HashMap;
......@@ -632,6 +633,7 @@ public class RulesTest {
assertEquals(1, affectedItem.getState(), DELTA, "Change of item state should set the state of the affected item");
}
@Disabled("Not working reliably, need to be made more robust")
@Test
public void testCronJobRule() {
Rule rule = new Rule();
......
/docs/ragdoc/
{% block footer %}
<p>{% if config.copyright %}
<small>{{ config.copyright }}<br></small>
{% endif %}
<hr>
Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
{% if page and page.meta and page.meta.revision_date %}
<small><br><i>Last updated {{ page.meta.revision_date.strftime('%B %d, %Y at %H:%M') }}</i></small>
{% endif %}
</p>
{% endblock %}
# The eraser-DSL
Find a general description in the [Model description](Model-description) page, and the parser ([dev](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/main/jastadd/eraser.parser), [master](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/master/eraser-base/src/main/jastadd/eraser.parser)) and printing ([dev](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/main/jastadd/Printing.jrag), [master](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/master/eraser-base/src/main/jastadd/Printing.jrag)).
The DSL is line-based, so on each line, one model element will be defined.
All shown specifications for one element are optional and can be reordered, but at least `id` must be present to identify the element.
References to other elements are done by using their IDs as strings, and can be forward references.
The following elements can be described (in the master branch if not otherwise noted):
## ChannelType
```
ChannelType: id="" label="" description="" itemType="" category="" readyOnly;
```
- The item type has to be one of the [item names defined for Eclipse Smart Home](https://www.eclipse.org/smarthome/documentation/concepts/items.html)
- The category can either be one of the [categories defined for Eclipse Smart Home](https://www.eclipse.org/smarthome/documentation/concepts/categories.html#channel-categories), or a custom one.
- Both item type and category are defined as enums ([dev](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/main/jastadd/enums.jadd), [master](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/master/eraser-base/src/main/jastadd/enums.jadd))
- The flag `readOnly` is currently without any effect
## Channel
```
Channel: id="" type="" links=["ITEM_ID", "ITEM_ID"];
```
- `type` must reference a [ChannelType](#channeltype), and must be set
- `links` must reference [items](#item)
## Group
```
Group: id="" groups=["GROUP_ID", "GROUP_ID"] items=["ITEM_ID", "ITEM_ID"] aggregation=AGG;
```
- `groups` must reference other [groups](#group)
- `items` must reference [items](#item)
- `AGG` in `aggregation` can either be one of `EQUALITY`, `AVG`, `MAX`, `MIN`, `SUM`; or one of `AND`, `OR`, `NAND`, `NOR`, followed by a list of arguments surrounded by round brackets, e.g., `aggregation = AVG` or `aggregation = AND ("off", "on")`. The semantics are described in the [openHAB docs](https://www.openhab.org/docs/configuration/items.html#group-type-and-state)
## Item
```
ITEM_TYPE Item: id="" label="" state="" category="" topic="" controls=["ITEM_ID", "ITEM_ID"] metaData={"key":"value"};
```
- `ITEM_TYPE` can be one of the following: `Color`, `Contact`, `DateTime`, `Dimmer`, `Image`, `Location`, `Number`, `Player`, `RollerShutter`, `String`, `Switch`. If left out, the item will behave as if `String` was used.
- `state` is given as a string and will be interpreted depending on the type of the item
- `topic` implicitely defines a [MQTT](mqtt) topic which is used to update the item in both directions. The full topic is prefixed with `incoming`-prefix if receiving updates from openHAB, and `outgoing`-prefix if sending updates to openHAB
- `category` is currently not used
- `controls` is only available on `dev`. It defines connection of one item controls the state of another item as the state of the first item changes.
- `metaData` contains key-value-pairs comprising its meta data
## Parameter
```
Parameter: id="" label="" description="" type="" default="" required;
```
- `type` has to be one of the following: `Boolean`, `Decimal`, `Integer`, `Text`
- `default` is currently an arbitrary String, thus it is not checked whether it matches the type
## ThingType
```
ThingType: id="" label="" description="" parameters=["PARAM_ID", "PARAM_ID"] channelTypes=["CHANNEL_TYPE_ID", "CHANNEL_TYPE_ID"];
```
- `parameters` must reference [parameters](#parameter)
- `channelTypes` must reference [channel types](#channeltype)
## Thing
```
Thing: id="" label="" type="" channels=["CHANNEL_ID", "CHANNEL_ID"] ;
```
- `type` must reference a [thing type](#thingtype), and must be set
- `channels` must reference [channels](#channel)
## Mqtt
```
Mqtt: incoming="" outgoing="" host="";
```
- MQTT is used to communicate/synchronize with openHAB
- `incoming` defines the prefix for received MQTT messages, see [topic description in item](#item). Defaults to an empty string, which should be avoided.
- `outgoing` defines the prefix for sent MQTT messages, see [topic description in item](#item). Defaults to an empty string, which should be avoided.
- `host` defines the URL of the host running an MQTT broker. The port will be set to `1883` and cannot be changed.
## Influx
```
Influx: user="" password="" dbName="" host="" ;
```
- Influx is used to store historical item states
- `user` and `password` define connection credentials
- `dbName` defines the name of the database to use
- `host` defines the URL of the host running an InfluxDB. The port will be set to `8086` and cannot be changed.
# Inner workings
Please also refer to the [API documentation](ragdoc/index.html)
## openHAB synchronization via MQTT
MQTTRoot contains `incomingPrefix`, `outgoingPrefix` and `host` tokens.
Items have a MQTT-topic associated, both from which they get updates via MQTT and publish changes of their state via MQTT.
Eraser will subscribe to `incomingPrefix/#`, i.e., every update of topics starting with `incomingPrefix` is listened to.
Also, if the a new host is set (`MQTTRoot.Host`), the connection is re-established.
Listening means, that upon a new message, the topic string is resolved to a non-terminal MQTT-topic using `Root.resolveTopic`. This topic non-terminal has a reference to its item, which in turn has its state updated with the body of the MQTT message. Each different item type has its own way to set its state from a string (`Item.setStateFromString`).
Whenever a state of an item is set from within the KB, this change will be published via MQTT, if `shouldSendState` is set to true, or the default setting (`DefaultShouldSendState`) is set to true.
The topic will be prefix with `outgoingPrefix`. Every item type has its own way to print its state to a string (`Item.getStateAsString`), which is used as a MQTT message body.
## Item state history via InfluxDB
Connecting to Influx is done with the Java binding `org.influxdb:influxdb-java`. To represent the data to send and receive, `ItemPoints` are used defined in the `ItemHistoryPoints.jrag` aspect file.
There is one class for each item type capturing the specifics for translating item states to data points.
A pretty cool way to add new behavior when setting the item state is to use JastAdd's refinement:
```
refine MQTT public void Item.sendState() throws Exception {
refined();
getRoot().getInfluxRoot().influxAdapter().write(pointFromState());
}
```
This adds writing a new data point to influx after the former behavior (which includes sending the update via MQTT, as described above).
However, those two are intended to be independent of each other.
For retrieving the history, an asynchronous approach was chosen and is described in the following. The sequence diagram shows the important methods called, where ItemType denotes a specific subclass of Item, e.g., `ColorItem` or `ItemWithDoubleState`, and Item denotes the general, common superclass `Item`.
![item-history](img/item-history.png)
To get the history, `getHistory` is called on a specific item returning a list of datapoints of a matching type, e.g., a list of `DoubleStatePoint` for an `ItemWithDoubleState`.
The code in `getHistory` is actuall the same for every type, namely: `return proxied(this::_history);`
This calls `proxied` (shown in yellow above) which first asynchronously updates the history and returns the current value using `_history()` (shown in orange).
For updating the history, first `historyUpdating` is checked, and if appropriate, the current InfluxAdapter is used to invoke `asyncQuery` and use the return value to set a new `history_token`.
This token contains the history, and is returned by the `_history` attribute. If data was never fetched before, an empty list is returned.
## Rules
For events in rules to trigger, `ItemObserver` objects are used. The following figure shows three rules (A, B, C) triggered by two different items (`item1` und `item2`):
![rules-object](img/rules-object.png)
To get those wirings, the following code can be used:
```java
Root model = createModel();
NumberedItem item1 = createNumberedItem();
ColorItem item2 = createColorItem();
addToModel(model, item1);
addToModel(model, item2);
Rule rA = new Rule();
ItemStateNumberCheck check1 = new ItemStateNumberCheck(ComparatorType.GreaterOrEqualThan, 4);
ItemStateNumberCheck check2 = new ItemStateNumberCheck(ComparatorType.LessThan, 6);
rA.addCondition(new ItemStateCheckCondition(check1));
rA.addCondition(new ItemStateCheckCondition(check2));
rA.setAction(new LambdaAction(item -> System.out.println("A was triggered by " + item.getLabel())));
rA.setAction(new LambdaAction(item -> item.disableSendState()));
rA.activateFor(item1);
Rule rB = new Rule();
Rule rC = new Rule();
// .. conditions, actions of B and C
rB.activateFor(item1);
rB.activateFor(item2);
rC.activateFor(item1);
```
Changes to `item1` trigger all three rules, whereas changing `item2` only triggers the second rule B.
Triggering a rule works as follows:
![rules-sequence](img/rules-sequence.png)
When the state of `item1` is changed, its observer checks, whether the new change is different to the previous change. If that is the case, it triggers all associated rules, i.e, it calls `rule.trigger()` for every rule in the `TriggeredRuleList`.
The rule checks all conditions (if any), and if all hold, executes all actions (if any).
In this shown case, `item1` was changed, thus all rules `rA`, `rB`, and `rC` are triggered, if the state has changed. The full sequence send for the two latter events is not shown above for brevity.
### Condition Types
Currently only one condition is supported, which compares the state of an ItemWithDoubleState with a given constant. It borrows its functionality from `ItemStateCheck`, which has a comparator (`>=`, `>`, `=`, `!=`, `<`, `<=`) and a double constant.
### Action Types
| Name | Children | Description |
|------|----------|-------------|
| Action | _none_ | Abstract super class for all actions |
| LambdaAction | <ul><li>`<Lambda:Action2EditConsumer>` An arbitrary lambda consuming an `Item`</li></ul> | Does arbitrary stuff, use only if none of the classes below fit. |
| TriggerRuleAction | <ul><li>`rel Rule -> Rule` Relation to the rule to trigger</li></ul> | Triggers another rule passing the same item to it. |
| _SetStateAction_ (abstract) | <ul><li>`rel AffectedItem -> Item` The item to change the state of</li></ul> | Abstract class for changing the state of one affected item. |
| SetStateFromConstantStringAction | <ul><li>`rel AffectedItem -> Item` (from SetStateAction)</li><li>`<NewState:String>` The new state to set</li></ul> | Sets the given constant as the new state for the affected item |
| SetStateFromLambdaAction | <ul><li>`rel AffectedItem -> Item` (from SetStateAction)</li><li>`<NewStateProvider:NewStateProvider>` A lambda to get the new state</li></ul> | Sets the given variable as the new state for the affected item |
| SetStateFromTriggeringItemAction | <ul><li>`rel AffectedItem -> Item` (from SetStateAction)</li></ul> | Sets the state of the triggering item as the new state for the affected item |
| SetStateFromItemsAction | <ul><li>`rel AffectedItem -> Item` (from SetStateAction)</li><li>`<Combinator:ItemsToStringFunction>` A function taking a list of items and returning a string</li><li>`rel SourceItem* -> Item` A list of input items</li></ul> | Uses the combinator to combine all input items to a new state, which is set for the affected item |
| AddDoubleToStateAction | <ul><li>`rel AffectedItem -> Item` (from SetStateAction)</li><li>`<Increment:double>` A constant value to add</li></ul> | Increments the state of the affected item by the given constant increment |
| MultiplyDoubleToStateAction | <ul><li>`rel AffectedItem -> Item` (from SetStateAction)</li><li>`<Multiplier:double>` A constant value to multipy</li></ul> | Multiplies the state of the affected item by the given constant multiplier |
# Learner
_This document is only available in German_
Der Learner ist ein Interface zur Encog Bibliothek. Er dient nur dazu Modelle zu traineren und zu Laufzeit des Systems anzupassen. Im Folgenden zeigen wir Beispiele wie der Learner zu Verwenden ist.
## Training mit externen Datensatz
```java
learner.setCsvFolderPath("/csvs");
ArrayList<Integer> targetColumns = new ArrayList<>();
targetColumns.add(5);
targetColumns.add(7);
int modelID = 1;
learner.loadDataSet("test1", targetColumns, modelID);
ArrayList<Integer> inputMaxes = new ArrayList<>();
inputMaxes.add(100); // e.g., first Columns is light intensity sensor with max value of 100
// ...
ArrayList<Integer> inputMins = new ArrayList<>();
inputMins.add(0); // e.g., first Columns is light intensity sensor with min value of 0
// ...
ArrayList<Integer> inputMaxes = new ArrayList<>();
targetMaxes.add(255); // e.g., first output value is R value of lamp with max value of 255
// ...
ArrayList<Integer> inputMins = new ArrayList<>();
targetMins.add(0); // e.g., first output value is R value of lamp with min value of 0
// ...
int inputCount = 5;
int outputCount = 2;
int hiddenCount = 1;
int hiddenNeuronCount = 4;
learner.train(inputCount, outputCount, hiddenCount, hiddenNeuronCount, imodelID, inputMaxes, inputMins, targetMaxes, targetMins);
Model model = learner.getTrainedModel(modelID);
```
## Training mit Daten von interner Datenbank
```java
ArrayList<Integer> targetColumns = new ArrayList<>();
targetColumns.add(5);
targetColumns.add(7);
int modelID = 1;
ArrayList<Integer> inputMaxes = new ArrayList<>();
inputMaxes.add(100); // e.g., first Columns is light intensity sensor with max value of 100
// ...
ArrayList<Integer> inputMins = new ArrayList<>();
inputMins.add(0); // e.g., first Columns is light intensity sensor with min value of 0
// ...
ArrayList<Integer> inputMaxes = new ArrayList<>();
targetMaxes.add(255); // e.g., first output value is R value of lamp with max value of 255
// ...
ArrayList<Integer> inputMins = new ArrayList<>();
targetMins.add(0); // e.g., first output value is R value of lamp with min value of 0
...
int inputCount = 5;
int outputCount = 2;
int hiddenCount = 1;
int hiddenNeuronCount = 4;
Table table = database.getData("query"); // Database Klasse, Methode und Table existieren nicht, nur ein Beispiel
double [][] data = table.toArray(); // Methode und Table existieren nicht, nur ein Beispiel
learner.train(data, inputCount, outputCount, hiddenCount, hiddenNeuronCount, imodelID, inputMaxes, inputMins, targetMaxes, targetMins, targetColumns);
Model model = learner.getTrainedModel(modelID);
```
## Retraining
```java
ArrayList<Integer> targetColumns = new ArrayList<>();
targetColumns.add(5);
targetColumns.add(7);
int modelID = 1;
data[][] dataVector = database.getNewestVector(); // Database Klasse und Methode existieren nicht nur ein Beispiel
learner.reTrain(dataVector, targetColumns, modelID);
```
## Inference
Bei der Nutzung der von Machine Learning muss in vielen Fällen die Einagbe und Ausgaben normalisiert bzw. denormalisiert werden.
Die Normalisierer können vom Learner angefragt werden mit getNormalizerInput und getNormalizerTar angefragt werden.
# Machine Learning Basics
The relevant parts of the grammar ([dev](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/main/jastadd/main.relast) | [master](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/main/jastadd/main.relast)) are the following (updated on Feburary, 11th 2019):
## General information machine learning
```
abstract MachineLearningModel ::= <OutputApplication:DoubleDoubleFunction> ;
abstract ItemPreference ::= ;
ItemPreferenceColor : ItemPreference ::= <PreferredHSB:TupleHSB> ;
ItemPreferenceDouble : ItemPreference ::= <PreferredValue:double> ;
rel ItemPreference.Item -> Item ;
// neural network
NeuralNetworkRoot : MachineLearningModel ::= InputNeuron* HiddenNeuron* OutputLayer ;
OutputLayer ::= OutputNeuron* <Combinator:DoubleArrayDoubleFunction> ;
abstract Neuron ::= Output:NeuronConnection* ;
NeuronConnection ::= <Weight:double> ;
InputNeuron : Neuron ;
HiddenNeuron : Neuron ::= <ActivationFormula:DoubleArrayDoubleFunction> ;
OutputNeuron : HiddenNeuron ::= <Label:String> ;
rel NeuronConnection.Neuron <-> Neuron.Input* ;
rel InputNeuron.Item -> Item ;
rel OutputLayer.AffectedItem -> Item ;
// decision tree
DecisionTreeRoot : MachineLearningModel ::= RootRule:DecisionTreeRule ;
abstract DecisionTreeElement ::= Preference:ItemPreference*;
abstract DecisionTreeRule : DecisionTreeElement ::= Left:DecisionTreeElement Right:DecisionTreeElement <Label:String> ;
ItemStateCheckRule : DecisionTreeRule ::= ItemStateCheck ;
abstract ItemStateCheck ::= <Comparator:ComparatorType> ;
ItemStateNumberCheck : ItemStateCheck ::= <Value:double> ;
ItemStateStringCheck : ItemStateCheck ::= <Value:String> ;
DecisionTreeLeaf : DecisionTreeElement ::= <ActivityIdentifier:int> <Label:String> ;
// dummy model
DummyMachineLearningModel : MachineLearningModel ::= <Current:DecisionTreeLeaf> ;
```
A machine learning model implements a classification task, thus it has an attribute `classify` returning a general class `Leaf` whose type depends to the specific model.
This leaf represents the result of a classification, and comprises a double value and a set of `ItemPreference`s.
Such a double value can be scaled using the `OutputApplication`, which is a function mapping one double value to another.
If the neural network only outputs values from 0 to 3, but a range from 0 to 100 is needed, the OutputApplication function can multiply the result with 33.
## Neural networks
A neural network is, generally speaken, a set of neurons connected through weighted connections.
Those neurons are typically put into layers such that connections of neurons are only possible between neurons in adjecent layers.
Currently in `eraser`, only the output layer is explicitely modelled.
Neurons are either Input-, Hidden-, or OutputNeurons.
InputNeurons typically form the first (implicit) layer and are connected to Items, thus get their state from the state of the connected Item.
HiddenNeurons are between input and output, and have an individual `ActivationFormula` taking all input values from the neuron and return its state.
OutputNeurons also have an activation formula, and are children of the `OutputLayer`.
In this output layer, a `Combinator` is defined, which merges the values of all output neurons into one double value as result of the whole neural network.
### Construction of a neural network
Most of the code snippets in this section are taken from [NeuralNetworkTest in the `dev` branch](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/test/java/de/tudresden/inf/st/eraser/NeuralNetworkTest.java).
To construct a neural network, first the root element needs to be created using `createEmpty`, which sets the output application function to the identity function:
```
NeuralNetworkRoot neuralNetworkRoot = NeuralNetworkRoot.createEmpty()
```
Then all neurons are created using the matching type, e.g.:
```java
InputNeuron inputNeuron = new InputNeuron();
HiddenNeuron hiddenNeuron = new HiddenNeuron();
hiddenNeuron.setActivationFormula(inputs -> 4 * inputs[0]);
OutputNeuron outputNeuron1 = new OutputNeuron();
outputNeuron1.setLabel("first");
outputNeuron1.setActivationFormula(inputs -> inputs[0] > 4 ? 1d : 0d);
```
Afterwards, the connections are set using the convenience method `connectTo` taking the target neuron and a weight:
```java
inputNeuron.connectTo(hiddenNeuron, 1);
hiddenNeuron.connectTo(outputNeuron1, 0.4);
```
As an important next step, the output layer and all neurons are added to the model:
```java
neuralNetworkRoot.addInputNeuron(inputNeuron);
neuralNetworkRoot.addHiddenNeuron(hiddenNeuron);
OutputLayer outputLayer = new OutputLayer();
outputLayer.addOutputNeuron(outputNeuron1);
neuralNetworkRoot.setOutputLayer(outputLayer);
```
Connect the items to the neural network.
This will assign the first item in the list to the first input neuron, the second to the second, and so on.
```java
Item item;
neuralNetworkRoot.connectItems(Arrays.asList(item));
```
The combinator function is set using a lambda expression:
```java
outputLayer.setCombinator(outputs -> {
int n = 0;
for (double d : outputs) {
n = (n << 1) | (d == 0 ? 0 : 1);
}
return (double) n;
});
```
Finally, the affected item needs to be set in the output layer:
```java
Item affectedItem;
outputLayer.setAffectedItem(affectedItem);
```
To verify, that the network was correctly built, the `check` method can be used.
It will check the various formulas (output, combinator, activation), valid connections, set weights and connected items.
The return value is `true` if no errors were found, and `false` otherwise.
In case of any warnings or errors, they are printed out using the logger.
```java
boolean everythingValid = neuralNetworkRoot.check();
```
### Using a neural network
Once a model has been built, it can be used either to recognize activities, or to learn preferences:
```java
Root model;
// either recognize activities, ...
model.getMachineLearningRoot().setActivityRecognition(neuralNetworkRoot);
// ... or learn preferences
model.getMachineLearningRoot().setPreferenceLearning(neuralNetworkRoot);
```
Once add the complete model, the classification can be invoked:
```java
Leaf leaf = neuralNetworkRoot.classify();
```
Depending on the use case, the leaf can either be interpreted as an activity, or as an item preference.
For an activity, the classification result is used as an acitivty identifier:
```java
int identifier = leaf.getActivityIdentifier();
Optional<Activity> activity = model.resolveActivity(identifier);
```
Alternatively, the result can be a preference as written in the following.
In case of a neural network, currently only one item will be affected (the one set as AffectedItem in the output layer).
```java
List<ItemPreference> preferences = leaf.computePreferences();
for (ItemPreference p : preferences) {
p.apply();
}
```
## Decision Trees
A decision tree is a tree of decisions.
Every decision rule refers to an item and compares the state of this item to a given constant.
The leaf of the tree don't contain any decisions anymore, instead they capture the classification result.
### Construction of a decision tree
Like above, most of the code was taken from the according test, in this case the [DecisionTreeTest](https://git-st.inf.tu-dresden.de/OpenLicht/eraser/blob/dev/eraser-base/src/test/java/de/tudresden/inf/st/eraser/DecisionTreeTest.java).
To begin with, construct a new `DecisionTreeRoot`.
```java
DecisionTreeRoot dtroot = new DecisionTreeRoot();
```
Next, create your first decision.
In this case, the rule will decided whether the state of the referenced item is below or above 4.
Instead of the leafs, other rules could be inserted referencing the same or another Item.
```java
DecisionTreeLeaf isLessThanFour = new DecisionTreeLeaf();
DecisionTreeLeaf isFourOrGreater = new DecisionTreeLeaf();
ItemStateNumberCheck check = new ItemStateNumberCheck(ComparatorType.LessThan, 4f);
JastAddList<ItemPreference> preferences = new JastAddList();
dtroot.setRootRule(new Rule(preferences, isLessThanFour, isFourOrGreater, "check item1", check));
```
For every leaf, its label, the resulting activity identifier and item preferences have to be set.
```java
Item affectedItem;
isLessThanFour.setLabel("less than four");
isLessThanFour.setActivityIdentifier(1);
isLessThanFour.addItemPreference(new ItemPreferenceDouble(2, affectedItem));
isFourOrGreater.setLabel("four or greater");
isFourOrGreater.setActivityIdentifier(3);
isFourOrGreater.addItemPreference(new ItemPreferenceColor(new TupleHSB(1, 2, 3), affectedItem));
```
The automatic connection of items to all elements of a decision tree is currently not supported.
Instead the items can only be set manually.
```java
Item item;
try {
DecisionTreeRoot.connectItems(Arrays.asList(item));
} catch(UnsupportedOperationException e) {
check.setItem(item);
}
```
### Using a decision tree
The result of the classification is the leaf at the end of the "decision path".
If the item has for example a state of 3, the leaf `isLessThanFour` will be returned.
```java
Leaf leaf = dtroot.classify();
```
A `DecisionTreeLeaf` directly has an activity identifier and item preferences attached to it, thus can be used in the same way as described [above](#using-a-neural-network).
# Model description
The latest version of the model: [master](https://git-st.inf.tu-dresden.de/rschoene/eraser/blob/master/eraser-base/src/main/jastadd/main.relast) | [dev](https://git-st.inf.tu-dresden.de/rschoene/eraser/blob/dev/eraser-base/src/main/jastadd/main.relast)
Below, all parts of the model are described in more detail. Each part resides in a separate grammar file.
## Main
Root ::= OpenHAB2Model User* MqttRoot InfluxRoot MachineLearningRoot Rule* Location* ;
- `OpenHAB2Model`: Content from openHAB
- `User*`: Users in the system (work in progress)
- `MqttRoot`: Everything needed for MQTT (communication with openHAB)
- `InfluxRoot`: Everything needed for Influx (time series database for item state history)
- `MachineLearningModelRoot`: The learned models for activity recognition and preference resolving
- `Rule*`: Self-made ECA rules
- `Location*`: Locations associated with users, activities, preferences (work in progress)
## openHAB
> OpenHAB2Model ::= Thing* Group* ThingType* ChannelType* ChannelCategory* ItemCategory* /ActivityItem:Item/
The structure is mostly extracted from openHAB. There are some abstract superclasses to share common token, such as `ID`, `label` or `description`. Some differences to openHAB include:
- Links are not modelled explicitely, but instead the items are directly referenced within channels
- Item type was reified instead of a token describing the type. This has the advantage, that the state can be used in a more type-safe way. On the downside, more code is required in some instances, such as for item history, and there is no easy `getState` method.
Explainations for the other types:
- **Things** represent physical entities, such as a Hue lamp, a complex (or simple) sensor, a monitor, a webservice. They are typed and described by ThingTypes.
- **ThingTypes** describe the blueprint for a Thing, like a class does for objects in OO languages. They also have **ChannelTypes** describing possible capabilities.
- **Items** are the capabilities of the things, e.g., the color temperature of a lamp, the measured temperature value of a sensor, a method of a webservice (the last one is probably a bad design). They have a type as described above.
- Things have **Channels** (one for each ChannelType of their ThingType) linking their effective capabilities to items via **Links**, such that those items represent the current value of this capability. A channel has a **ChannelCategory** describing the capability, e.g., `BatteryLevel`, `Switch`, `Presence`
- **Groups** contain other groups, and other items
## MQTT
- Topics are a flat list, referencing items to be updated upon a message in this topic
- All topics have one common prefix to subscribe to topics, and another common prefix to publish topics
## Influx
Basically, here are just some tokens containing information for connection to Influx, such as username, password, host
## Machine Learning
Handling of machine learning models currently undergoes a change.
There is a distinction between internal and external models, both implement a encoder interface to get data into those models, and decoder interface to extract data from the models.
There are two internal models which are supported: decision trees and neural networks.
Decision tree are modelled straight-forward, there are inner nodes and leafs. Inner nodes reference an item and check a certain condition for this item to decide whether to follow the left or the right path of the inner node. The classification is equal to the leaf node at the end of the path.
Neural networks are also straight-forward, but probably inefficient for bigger networks in the current implementation. Every neuron is a non-terminal which has ingoing and outgoing connections (using bidirectional relations), input neurons reference an item and propagate its state, hidden and output neurons have an activation function, bias neurons always return a constant value. The output layer contains all output neurons and has a combinator function to calculate the classification result.
## Rules
Simple ECA rules can be modelled. A rule has a list of conditions and a list of actions. An event can be triggered by some items (via their ItemObserver), i.e., whenever an item changes its state to a different value. Conditions use a logical expression, which can contain simple mathematical expressions, comparisons, and simple logical operations. Actions are organized currently by their purpose, but are wll be simplified to reference only affected items and a number expression to calculate the new value for those items.
# Configuration
This page describes the main configuration options when starter eraser.
Whenever a file is specified here, it will be a combination of `file` and `external`. With `external` set to `true`, the given path is used to access the file system, using the directory of the `eraser.starter` module as current directory for relative paths. If `external` is set to `false` the file is looked up in the bundled JAR file.
## Starting configuration `starter-settings.yaml`
In this file, everything is being configured, i.e., which components will be started, and how they are set up.
It has global options (mostly boolean) and sections for components, both declared on the top-level.
### Global options
| Name | Type | Description | Default |
|-----------------|---------|--------------------------------------------------------------------|---------|
| `useMAPE` | Boolean | Start the feedback loop | True |
| `sharedLearner` | Boolean | Use the same learner instance for activity and preference learning | False |
| `mqttUpdate` | Boolean | Get updates from openHAB into the knowledge base | True |
| `initModelWith` | Choice | Method to initialize model. Possible values: "load", "openhab" | "load" |
### Section `rest`
This section configures the REST server providing an API to interact with eraser.
| Name | Type | Description | Default |
|---------------------|---------|------------------------------------------------------------------------------------------|---------|
| `use` | Boolean | Start the REST server | True |
| `port` | Integer | Port of the REST server | 4567 |
| `createDummyMLData` | Boolean | Add some dummy data for activities and events. Only effective when using the REST server | False |
### Section `load`
This section defines how to initialize eraser when using the [option `initModelWith`](#global-options) with the value "load".
| Name | Type | Description | Default |
|------------|----------|-------------------------------------------------------------------|---------|
| `file` | Filename | File to read in, expected format: `.eraser` | _None_ |
| `external` | Boolean | False: Use file bundled in the JAR. True: Use file in filesystem. | False |
### Sections `activity` and `preference`
These section have equivalent options and define the activity recogntion taking place in the Analyze phase, and the preference learning triggered in the Plan phase respectively.
| Name | Type | Description | Default |
|------------|----------|--------------------------------------------------------------------|---------|
| `file` | Filename | File to read in, expected format: `.eraser` | _None_ |
| `external` | Boolean | False: Use file bundled in the JAR. True: Use file in filesystem. | False |
| `dummy` | Boolean | Use dummy model in which the current activity is directly editable | false |
| `id` | Integer | Model id. Currently unused | 1 |
### Section `openhab`
This section configures the communication with openHAB.
| Name | Type | Description | Default |
|---------------------|--------|--------------------------------------------------------------|---------|
| `url` | String | The URL from which to import and at which openHAB is running | _None_ |
| `metadataNamespace` | String | The metadata namespace used for items | _None_ |
## Configuring start of eraser
To build eraser, execute
```bash
./gradlew :eraser.starter:installDist
```
This will create bundled JARs and a script to launch the starter. Both will be located in `eraser.starter/build/install/eraser.starter/`.
To start, change to this directory and invoke
```bash
./bin/eraser.starter -f starter-setting.yaml
```
Beforehand, the configuration has to be changed according to your needs.
# Contributing
Please also refer to the [API documentation](ragdoc/index.html)
Steps to create the initial multi-project setup from a single-project setup
- create a new Gradle project named `eraser` using IntelliJ
- manually create subdirectory `eraser-base`, move `src/` to this directory
- change `build.gradle` to
```Groovy
allprojects {
group = 'de.tudresden.inf.st'
version = '1.0.0-SNAPSHOT'
}
subprojects {
apply plugin: 'java'
sourceCompatibility = 1.8
targetCompatibility = 1.8
task packageSources(type: Jar) {
classifier = 'sources'
from sourceSets.main.allSource
}
artifacts.archives packageSources
configurations {
testArtifacts.extendsFrom testRuntime
}
task testJar(type: Jar) {
classifier "test"
from sourceSets.test.output
}
artifacts {
testArtifacts testJar
}
}
```
- create new `eraser-base/build.gradle` with the following content
```Groovy
repositories {
mavenCentral()
}
apply plugin: 'jastadd'
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
}
buildscript {
repositories.mavenLocal()
repositories.mavenCentral()
dependencies {
classpath 'org.jastadd:jastaddgradle:1.12.2'
}
}
jastadd {
configureModuleBuild()
modules "jastadd_modules"
module = "expressions"
astPackage = 'de.tudresden.inf.st.eraser.jastadd.model'
genDir = 'src/gen/java'
buildInfoDir = 'src/gen-res'
}
```
- create new directory `eraser-base/src/main/jastadd` and put in JADD and JRAG files
- create new `eraser-base/jastadd_modules` with the following content:
```Groovy
module("expressions") {
java {
basedir "src/main/java/"
include "**/*.java"
}
jastadd {
basedir "src/main/jastadd/"
include "**/*.ast"
include "**/*.jadd"
include "**/*.jrag"
}
}
```
- optionally create a `.gitignore` to not commit `.idea` and `.gradle` directories
- optionally create a Main Java class
pages/docs/img/config-eraser-binding.png

68.7 KiB

pages/docs/img/config-mqtt-binding.png

36.5 KiB

pages/docs/img/item-history.png

18.5 KiB

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="14.3.0">
<zoom_level>10</zoom_level>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>50</x>
<y>100</y>
<w>150</w>
<h>30</h>
</coordinates>
<panel_attributes>_:Caller-Thread_</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>370</x>
<y>100</y>
<w>150</w>
<h>30</h>
</coordinates>
<panel_attributes>_:Updater-Thread_</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>110</x>
<y>120</y>
<w>30</w>
<h>80</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>110</x>
<y>180</y>
<w>20</w>
<h>340</h>
</coordinates>
<panel_attributes>
layer=-1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>120</x>
<y>180</y>
<w>210</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
TypedItem.getHistory</panel_attributes>
<additional_attributes>10.0;40.0;40.0;40.0;40.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>120</x>
<y>230</y>
<w>150</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
Item.proxied</panel_attributes>
<additional_attributes>20.0;40.0;40.0;40.0;40.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>430</x>
<y>120</y>
<w>30</w>
<h>290</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;270.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>430</x>
<y>390</y>
<w>20</w>
<h>60</h>
</coordinates>
<panel_attributes/>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>130</x>
<y>370</y>
<w>320</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;-
InfluxAdapter.asyncQuery</panel_attributes>
<additional_attributes>300.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>130</x>
<y>280</y>
<w>310</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
ItemType.influxMeasurementName</panel_attributes>
<additional_attributes>10.0;40.0;40.0;40.0;40.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>130</x>
<y>320</y>
<w>200</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
ItemType.pointClass</panel_attributes>
<additional_attributes>10.0;40.0;40.0;40.0;40.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>440</x>
<y>390</y>
<w>220</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
Item.set_history_token</panel_attributes>
<additional_attributes>10.0;40.0;40.0;40.0;40.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLSpecialState</id>
<coordinates>
<x>430</x>
<y>460</y>
<w>20</w>
<h>20</h>
</coordinates>
<panel_attributes>type=termination</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>430</x>
<y>440</y>
<w>30</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>120</x>
<y>270</y>
<w>20</w>
<h>240</h>
</coordinates>
<panel_attributes>
layer=3
bg=yellow</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLNote</id>
<coordinates>
<x>290</x>
<y>220</y>
<w>120</w>
<h>60</h>
</coordinates>
<panel_attributes>/_history/
overloaded for
each Itemtype</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>250</x>
<y>230</y>
<w>60</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>40.0;10.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>130</x>
<y>440</y>
<w>20</w>
<h>60</h>
</coordinates>
<panel_attributes>
layer=5
bg=orange</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>130</x>
<y>400</y>
<w>210</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
syn ItemType._history</panel_attributes>
<additional_attributes>20.0;40.0;40.0;40.0;40.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>140</x>
<y>450</y>
<w>280</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
Item.fetchHistory(history_token)</panel_attributes>
<additional_attributes>10.0;40.0;30.0;40.0;30.0;10.0;10.0;10.0</additional_attributes>
</element>
</diagram>
pages/docs/img/rules-object.png

18.1 KiB

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="14.3.0">
<zoom_level>10</zoom_level>
<element>
<id>UMLClass</id>
<coordinates>
<x>360</x>
<y>90</y>
<w>160</w>
<h>50</h>
</coordinates>
<panel_attributes>item1:NumberItem
--
state: Double
</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>750</x>
<y>90</y>
<w>160</w>
<h>50</h>
</coordinates>
<panel_attributes>item2:ColorItem
--
state: TupleHSB</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>750</x>
<y>180</y>
<w>160</w>
<h>30</h>
</coordinates>
<panel_attributes>io2:ItemObserver
bg=orange</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>360</x>
<y>180</y>
<w>160</w>
<h>30</h>
</coordinates>
<panel_attributes>io1:ItemObserver
bg=yellow</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>430</x>
<y>130</y>
<w>30</w>
<h>70</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>810</x>
<y>130</y>
<w>30</w>
<h>70</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>350</x>
<y>200</y>
<w>60</w>
<h>180</h>
</coordinates>
<panel_attributes>lt=&lt;-
</panel_attributes>
<additional_attributes>10.0;160.0;40.0;160.0;40.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>420</x>
<y>200</y>
<w>100</w>
<h>170</h>
</coordinates>
<panel_attributes>lt=&lt;-
</panel_attributes>
<additional_attributes>80.0;150.0;80.0;130.0;10.0;130.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>480</x>
<y>60</y>
<w>470</w>
<h>320</h>
</coordinates>
<panel_attributes>lt=&lt;-
</panel_attributes>
<additional_attributes>390.0;300.0;450.0;300.0;450.0;20.0;190.0;20.0;190.0;190.0;10.0;190.0;10.0;150.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>610</x>
<y>200</y>
<w>230</w>
<h>190</h>
</coordinates>
<panel_attributes>lt=&lt;-
</panel_attributes>
<additional_attributes>10.0;170.0;50.0;170.0;50.0;80.0;210.0;80.0;210.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>200</x>
<y>350</y>
<w>160</w>
<h>30</h>
</coordinates>
<panel_attributes>rA:Rule
bg=yellow</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>220</x>
<y>480</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>aA1:Action</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>230</x>
<y>440</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>cA2:Condition</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>210</x>
<y>370</y>
<w>40</w>
<h>100</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;10.0;80.0;20.0;80.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>320</x>
<y>370</y>
<w>50</w>
<h>140</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>30.0;10.0;30.0;120.0;10.0;120.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>460</x>
<y>350</y>
<w>160</w>
<h>30</h>
</coordinates>
<panel_attributes>rB:Rule
</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>710</x>
<y>350</y>
<w>160</w>
<h>30</h>
</coordinates>
<panel_attributes>rC:Rule
bg=yellow</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>540</x>
<y>350</y>
<w>80</w>
<h>30</h>
</coordinates>
<panel_attributes>bg=orange
fg=white</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>460</x>
<y>350</y>
<w>90</w>
<h>30</h>
</coordinates>
<panel_attributes>bg=yellow
fg=white</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>230</x>
<y>400</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>cA1:Condition</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>210</x>
<y>370</y>
<w>40</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;10.0;40.0;20.0;40.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>220</x>
<y>520</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>aA2:Action</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>320</x>
<y>370</y>
<w>50</w>
<h>180</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>30.0;10.0;30.0;160.0;10.0;160.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>470</x>
<y>480</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>aA1:Action</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>570</x>
<y>370</y>
<w>50</w>
<h>140</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>30.0;10.0;30.0;120.0;10.0;120.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>480</x>
<y>400</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>cB1:Condition</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>460</x>
<y>370</y>
<w>40</w>
<h>60</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>10.0;10.0;10.0;40.0;20.0;40.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>470</x>
<y>520</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>aA2:Action</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>570</x>
<y>370</y>
<w>50</w>
<h>180</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>30.0;10.0;30.0;160.0;10.0;160.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>720</x>
<y>420</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>aA1:Action</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>820</x>
<y>370</y>
<w>50</w>
<h>80</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;&lt;&lt;-</panel_attributes>
<additional_attributes>30.0;10.0;30.0;60.0;10.0;60.0</additional_attributes>
</element>
</diagram>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment