Commit 165b3f66 authored by Thomas Michael Timmermanns's avatar Thomas Michael Timmermanns Committed by Thomas Michael Timmermanns

Added Coco and test.

Combined preResolveCocos and postResolveCococ cocos into class CNNArchCocos.
parent d4163365
......@@ -20,10 +20,33 @@
*/
package de.monticore.lang.monticar.cnnarch._cocos;
import de.monticore.lang.monticar.cnnarch._ast.ASTCNNArchNode;
import de.monticore.lang.monticar.cnnarch._symboltable.ArchitectureSymbol;
import de.se_rwth.commons.logging.Log;
public class CNNArchPreResolveCocos {
//check all cocos
public class CNNArchCocos {
public static CNNArchCoCoChecker createChecker() {
public static void checkAll(ArchitectureSymbol architecture){
ASTCNNArchNode node = (ASTCNNArchNode) architecture.getAstNode().get();
createPreResolveChecker().checkAll(node);
if (Log.getFindings().isEmpty()){
architecture.resolve();
if (Log.getFindings().isEmpty()){
createPostResolveChecker().checkAll(node);
}
}
}
public static CNNArchCoCoChecker createPostResolveChecker() {
return new CNNArchCoCoChecker()
.addCoCo(new CheckLayerInputs())
.addCoCo(new CheckIOAccessAndIOMissing())
.addCoCo(new CheckIOShape())
.addCoCo(new CheckArchitectureFinished());
}
public static CNNArchCoCoChecker createPreResolveChecker() {
return new CNNArchCoCoChecker()
.addCoCo(new CheckIOName())
.addCoCo(new CheckNameExpression())
......@@ -35,4 +58,4 @@ public class CNNArchPreResolveCocos {
.addCoCo(new CheckMethodName())
.addCoCo(new CheckMethodRecursion());
}
}
\ No newline at end of file
}
......@@ -20,13 +20,29 @@
*/
package de.monticore.lang.monticar.cnnarch._cocos;
public class CNNArchPostResolveCocos {
import de.monticore.lang.monticar.cnnarch._ast.ASTArchitecture;
import de.monticore.lang.monticar.cnnarch._symboltable.ArchitectureSymbol;
import de.monticore.lang.monticar.cnnarch.helper.ErrorCodes;
import de.se_rwth.commons.logging.Log;
public static CNNArchCoCoChecker createChecker() {
return new CNNArchCoCoChecker()
.addCoCo(new CheckLayerInputs())
.addCoCo(new CheckIOAccessAndIOMissing())
.addCoCo(new CheckIOShape());
public class CheckArchitectureFinished implements CNNArchASTArchitectureCoCo {
@Override
public void check(ASTArchitecture node) {
ArchitectureSymbol architecture = (ArchitectureSymbol) node.getSymbol().get();
if (!architecture.getBody().getOutputTypes().isEmpty()){
Log.error("0" + ErrorCodes.UNFINISHED_ARCHITECTURE + " The architecture is not finished. " +
"There are still open streams at the end of the architecture. "
, node.get_SourcePositionEnd());
}
if (architecture.getInputs().isEmpty()){
Log.error("0" + ErrorCodes.UNFINISHED_ARCHITECTURE + " The architecture has no inputs. "
, node.get_SourcePositionStart());
}
if (architecture.getOutputs().isEmpty()){
Log.error("0" + ErrorCodes.UNFINISHED_ARCHITECTURE + " The architecture has no outputs. "
, node.get_SourcePositionStart());
}
}
}
......@@ -22,7 +22,9 @@ package de.monticore.lang.monticar.cnnarch._cocos;
import de.monticore.lang.monticar.cnnarch._ast.ASTArchArgument;
import de.monticore.lang.monticar.cnnarch._symboltable.ArgumentSymbol;
import de.monticore.lang.monticar.cnnarch._symboltable.MethodDeclarationSymbol;
import de.monticore.lang.monticar.cnnarch.helper.ErrorCodes;
import de.se_rwth.commons.Joiners;
import de.se_rwth.commons.logging.Log;
public class CheckArgument implements CNNArchASTArchArgumentCoCo {
......@@ -30,9 +32,11 @@ public class CheckArgument implements CNNArchASTArchArgumentCoCo {
@Override
public void check(ASTArchArgument node) {
ArgumentSymbol argument = (ArgumentSymbol) node.getSymbol().get();
MethodDeclarationSymbol method = argument.getMethodLayer().getMethod();
if (argument.getParameter() == null){
Log.error("0"+ ErrorCodes.UNKNOWN_ARGUMENT + " Unknown Argument. " +
"Parameter with name '" + node.getName() + "' does not exist."
"Parameter with name '" + node.getName() + "' does not exist. " +
"Possible arguments are: " + Joiners.COMMA.join(method.getParameters())
, node.get_SourcePositionStart());
}
else {
......
......@@ -101,16 +101,16 @@ public class ArchTypeSymbol extends CommonSymbol {
return getDimensionSymbols().get(getChannelIndex());
}
public Optional<Integer> getWidth(){
return getWidthSymbol().getIntValue();
public Integer getWidth(){
return getWidthSymbol().getIntValue().get();
}
public Optional<Integer> getHeight(){
return getHeightSymbol().getIntValue();
public Integer getHeight(){
return getHeightSymbol().getIntValue().get();
}
public Optional<Integer> getChannels(){
return getChannelsSymbol().getIntValue();
public Integer getChannels(){
return getChannelsSymbol().getIntValue().get();
}
protected void setDimensionSymbols(List<ArchSimpleExpressionSymbol> dimensions) {
......
......@@ -70,10 +70,12 @@ public class IOLayerSymbol extends LayerSymbol {
definition.getConnectedLayers().add(this);
}
@Override
public boolean isInput(){
return getDefinition().isInput();
}
@Override
public boolean isOutput(){
return getDefinition().isOutput();
}
......
......@@ -86,6 +86,16 @@ public abstract class LayerSymbol extends CommonScopeSpanningSymbol {
}
}
public boolean isInput(){
//override by IOLayerSymbol
return false;
}
public boolean isOutput(){
//override by IOLayerSymbol
return false;
}
/**
* only call after resolve():
* @return returns the non-empty atomic layers which have the output of this layer as input.
......@@ -229,7 +239,7 @@ public abstract class LayerSymbol extends CommonScopeSpanningSymbol {
*/
abstract public boolean isAtomic();
//only call after resolve
//only call after resolve; used in coco CheckLayerInputs to check the input type and shape of each layer.
abstract public void checkInput();
}
......@@ -337,6 +337,10 @@ public class MethodLayerSymbol extends LayerSymbol {
return getTValue(parameterName, ArchExpressionSymbol::getDoubleValue);
}
public Optional<Object> getValue(String parameterName){
return getTValue(parameterName, ArchExpressionSymbol::getValue);
}
public <T> Optional<T> getTValue(String parameterName, Function<ArchExpressionSymbol, Optional<T>> getValue){
Optional<ArgumentSymbol> arg = getArgument(parameterName);
Optional<VariableSymbol> param = getMethod().getParameter(parameterName);
......
......@@ -78,8 +78,8 @@ abstract public class PredefinedMethodDeclaration extends MethodDeclarationSymbo
//check input for convolution and pooling
protected static void errorIfInputSmallerThanKernel(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer){
if (!inputTypes.isEmpty()) {
int inputHeight = inputTypes.get(0).getHeight().get();
int inputWidth = inputTypes.get(0).getWidth().get();
int inputHeight = inputTypes.get(0).getHeight();
int inputWidth = inputTypes.get(0).getWidth();
int kernelHeight = layer.getIntTupleValue(AllPredefinedMethods.KERNEL_NAME).get().get(0);
int kernelWidth = layer.getIntTupleValue(AllPredefinedMethods.KERNEL_NAME).get().get(1);
......@@ -123,8 +123,8 @@ abstract public class PredefinedMethodDeclaration extends MethodDeclarationSymbo
int strideWidth = method.getIntTupleValue(AllPredefinedMethods.STRIDE_NAME).get().get(1);
int kernelHeight = method.getIntTupleValue(AllPredefinedMethods.KERNEL_NAME).get().get(0);
int kernelWidth = method.getIntTupleValue(AllPredefinedMethods.KERNEL_NAME).get().get(1);
int inputHeight = inputType.getHeight().get();
int inputWidth = inputType.getWidth().get();
int inputHeight = inputType.getHeight();
int inputWidth = inputType.getWidth();
int outputWidth;
int outputHeight;
......@@ -151,8 +151,8 @@ abstract public class PredefinedMethodDeclaration extends MethodDeclarationSymbo
int strideWidth = method.getIntTupleValue(AllPredefinedMethods.STRIDE_NAME).get().get(1);
int kernelHeight = method.getIntTupleValue(AllPredefinedMethods.KERNEL_NAME).get().get(0);
int kernelWidth = method.getIntTupleValue(AllPredefinedMethods.KERNEL_NAME).get().get(1);
int inputHeight = inputType.getHeight().get();
int inputWidth = inputType.getWidth().get();
int inputHeight = inputType.getHeight();
int inputWidth = inputType.getWidth();
int outputWidth = 1 + Math.max(0, ((inputWidth - kernelWidth + strideWidth - 1) / strideWidth));
int outputHeight = 1 + Math.max(0, ((inputHeight - kernelHeight + strideHeight - 1) / strideHeight));
......@@ -169,8 +169,8 @@ abstract public class PredefinedMethodDeclaration extends MethodDeclarationSymbo
private static List<ArchTypeSymbol> computeOutputShapeWithSamePadding(ArchTypeSymbol inputType, MethodLayerSymbol method, int channels){
int strideHeight = method.getIntTupleValue(AllPredefinedMethods.STRIDE_NAME).get().get(0);
int strideWidth = method.getIntTupleValue(AllPredefinedMethods.STRIDE_NAME).get().get(1);
int inputHeight = inputType.getHeight().get();
int inputWidth = inputType.getWidth().get();
int inputHeight = inputType.getHeight();
int inputWidth = inputType.getWidth();
int outputWidth = (inputWidth + strideWidth - 1) / strideWidth;
int outputHeight = (inputHeight + strideWidth - 1) / strideHeight;
......
......@@ -24,13 +24,13 @@ public enum VariableType {
METHOD_PARAMETER {
@Override
public String toString(){
return "parameter";
return "method parameter";
}
},
ARCHITECTURE_PARAMETER {
@Override
public String toString(){
return "globalVariable";
return "architecture parameter";
}
},
CONSTANT {
......
......@@ -41,5 +41,6 @@ public class ErrorCodes {
public static final String MISSING_MERGE = "x82479";
public static final String UNKNOWN_VARIABLE_NAME = "x65013";
public static final String DIFFERENT_RANGE_OPERATORS = "xA8289";
public static final String UNFINISHED_ARCHITECTURE = "x28B42";
}
......@@ -47,9 +47,9 @@ public class Add extends PredefinedMethodDeclaration {
List<String> range = computeStartAndEndValue(inputTypes, Rational::plus, Rational::plus);
return Collections.singletonList(new ArchTypeSymbol.Builder()
.channels(inputTypes.get(0).getChannels().get())
.height(inputTypes.get(0).getHeight().get())
.width(inputTypes.get(0).getWidth().get())
.channels(inputTypes.get(0).getChannels())
.height(inputTypes.get(0).getHeight())
.width(inputTypes.get(0).getWidth())
.elementType(range.get(0), range.get(1))
.build());
}
......@@ -65,9 +65,9 @@ public class Add extends PredefinedMethodDeclaration {
List<Integer> widthList = new ArrayList<>();
List<Integer> channelsList = new ArrayList<>();
for (ArchTypeSymbol shape : inputTypes){
heightList.add(shape.getHeight().get());
widthList.add(shape.getWidth().get());
channelsList.add(shape.getChannels().get());
heightList.add(shape.getHeight());
widthList.add(shape.getWidth());
channelsList.add(shape.getChannels());
}
int countEqualHeights = (int)heightList.stream().distinct().count();
int countEqualWidths = (int)widthList.stream().distinct().count();
......
......@@ -26,9 +26,9 @@ import de.monticore.lang.monticar.cnnarch._symboltable.VariableType;
public class AllPredefinedVariables {
public static final String IF_NAME = "If";
public static final String FOR_NAME = "For";
public static final String CARDINALITY_NAME = "Cardinality";
public static final String IF_NAME = "?";
public static final String FOR_NAME = "->";
public static final String CARDINALITY_NAME = "|";
public static final String TRUE_NAME = "true";
public static final String FALSE_NAME = "false";
......
......@@ -42,11 +42,11 @@ public class Concatenate extends PredefinedMethodDeclaration {
@Override
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
int height = inputTypes.get(0).getHeight().get();
int width = inputTypes.get(0).getWidth().get();
int height = inputTypes.get(0).getHeight();
int width = inputTypes.get(0).getWidth();
int channels = 0;
for (ArchTypeSymbol inputShape : inputTypes) {
channels += inputShape.getChannels().get();
channels += inputShape.getChannels();
}
List<String> range = computeStartAndEndValue(inputTypes, (x,y) -> x.isLessThan(y) ? x : y, (x,y) -> x.isLessThan(y) ? y : x);
......@@ -65,8 +65,8 @@ public class Concatenate extends PredefinedMethodDeclaration {
List<Integer> heightList = new ArrayList<>();
List<Integer> widthList = new ArrayList<>();
for (ArchTypeSymbol shape : inputTypes){
heightList.add(shape.getHeight().get());
widthList.add(shape.getWidth().get());
heightList.add(shape.getHeight());
widthList.add(shape.getWidth());
}
int countEqualHeights = (int)heightList.stream().distinct().count();
int countEqualWidths = (int)widthList.stream().distinct().count();
......
......@@ -39,9 +39,9 @@ public class Flatten extends PredefinedMethodDeclaration {
return Collections.singletonList(new ArchTypeSymbol.Builder()
.height(1)
.width(1)
.channels(inputTypes.get(0).getHeight().get()
* inputTypes.get(0).getWidth().get()
* inputTypes.get(0).getChannels().get())
.channels(inputTypes.get(0).getHeight()
* inputTypes.get(0).getWidth()
* inputTypes.get(0).getChannels())
.elementType(inputTypes.get(0).getElementType())
.build());
}
......
......@@ -38,7 +38,7 @@ public class GlobalPooling extends PredefinedMethodDeclaration {
return Collections.singletonList(new ArchTypeSymbol.Builder()
.height(1)
.width(1)
.channels(inputTypes.get(0).getChannels().get())
.channels(inputTypes.get(0).getChannels())
.elementType(inputTypes.get(0).getElementType())
.build());
}
......
......@@ -34,7 +34,7 @@ public class Pooling extends PredefinedMethodDeclaration {
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
return computeConvAndPoolOutputShape(inputTypes.get(0),
layer,
inputTypes.get(0).getChannels().get());
inputTypes.get(0).getChannels());
}
@Override
......
......@@ -38,9 +38,9 @@ public class Relu extends PredefinedMethodDeclaration {
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
return Collections.singletonList(
new ArchTypeSymbol.Builder()
.channels(inputTypes.get(0).getChannels().get())
.height(inputTypes.get(0).getHeight().get())
.width(inputTypes.get(0).getWidth().get())
.channels(inputTypes.get(0).getChannels())
.height(inputTypes.get(0).getHeight())
.width(inputTypes.get(0).getWidth())
.elementType("0", "oo")
.build());
}
......
......@@ -38,9 +38,9 @@ public class Sigmoid extends PredefinedMethodDeclaration {
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
return Collections.singletonList(
new ArchTypeSymbol.Builder()
.channels(inputTypes.get(0).getChannels().get())
.height(inputTypes.get(0).getHeight().get())
.width(inputTypes.get(0).getWidth().get())
.channels(inputTypes.get(0).getChannels())
.height(inputTypes.get(0).getHeight())
.width(inputTypes.get(0).getWidth())
.elementType("0", "1")
.build());
}
......
......@@ -38,9 +38,9 @@ public class Softmax extends PredefinedMethodDeclaration {
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
return Collections.singletonList(
new ArchTypeSymbol.Builder()
.channels(inputTypes.get(0).getChannels().get())
.height(inputTypes.get(0).getHeight().get())
.width(inputTypes.get(0).getWidth().get())
.channels(inputTypes.get(0).getChannels())
.height(inputTypes.get(0).getHeight())
.width(inputTypes.get(0).getWidth())
.elementType("0", "1")
.build());
}
......
......@@ -38,9 +38,9 @@ public class Split extends PredefinedMethodDeclaration {
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
ArchTypeSymbol inputShape = inputTypes.get(0);
int numberOfSplits = layer.getIntValue(AllPredefinedMethods.NUM_SPLITS_NAME).get();
int inputHeight = inputShape.getHeight().get();
int inputWidth = inputShape.getWidth().get();
int inputChannels = inputShape.getChannels().get();
int inputHeight = inputShape.getHeight();
int inputWidth = inputShape.getWidth();
int inputChannels = inputShape.getChannels();
int outputChannels = inputChannels / numberOfSplits;
int outputChannelsLast = inputChannels - (numberOfSplits-1) * outputChannels;
......@@ -69,7 +69,7 @@ public class Split extends PredefinedMethodDeclaration {
@Override
public void checkInput(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
if (inputTypes.size() == 1) {
int inputChannels = inputTypes.get(0).getChannels().get();
int inputChannels = inputTypes.get(0).getChannels();
int numberOfSplits = layer.getIntValue(AllPredefinedMethods.NUM_SPLITS_NAME).get();
if (inputChannels < numberOfSplits){
......
......@@ -38,9 +38,9 @@ public class Tanh extends PredefinedMethodDeclaration {
public List<ArchTypeSymbol> computeOutputTypes(List<ArchTypeSymbol> inputTypes, MethodLayerSymbol layer) {
return Collections.singletonList(
new ArchTypeSymbol.Builder()
.channels(inputTypes.get(0).getChannels().get())
.height(inputTypes.get(0).getHeight().get())
.width(inputTypes.get(0).getWidth().get())
.channels(inputTypes.get(0).getChannels())
.height(inputTypes.get(0).getHeight())
.width(inputTypes.get(0).getWidth())
.elementType("-1", "1")
.build());
}
......
......@@ -63,58 +63,14 @@ public class SymtabTest extends AbstractSymtabTest {
@Ignore
@Test
public void testFixedThreeInput(){
public void test3(){
Scope symTab = createSymTab("src/test/resources/valid_tests");
ArchitectureSymbol a = symTab.<ArchitectureSymbol>resolve(
"Alexnet_alt2",
"MultipleOutputs",
ArchitectureSymbol.KIND).orElse(null);
assertNotNull(a);
a.resolve();
a.getBody().getOutputTypes();
}
/*@Test
public void testThreeInput(){
Scope symTab = createSymTab("src/test/resources/architectures");
ArchitectureSymbol a = symTab.<ArchitectureSymbol>resolve(
"ThreeInputCNN_M14",
ArchitectureSymbol.KIND).orElse(null);
assertNotNull(a);
a.resolve();
a.getBody().getOutputTypes();
}
@Test
public void testFixedAlexnet(){
Scope symTab = createSymTab("src/test/resources/valid_tests");
ArchitectureSymbol a = symTab.<ArchitectureSymbol>resolve(
"Fixed_Alexnet",
ArchitectureSymbol.KIND).orElse(null);
assertNotNull(a);
a.resolve();
a.getBody().getOutputTypes();
}
@Test
public void testFixedResNeXt(){
Scope symTab = createSymTab("src/test/resources/valid_tests");
ArchitectureSymbol a = symTab.<ArchitectureSymbol>resolve(
"Fixed_ResNeXt50",
ArchitectureSymbol.KIND).orElse(null);
assertNotNull(a);
a.resolve();
a.getBody().getOutputTypes();
}
@Test
public void testFixedThreeInput(){
Scope symTab = createSymTab("src/test/resources/valid_tests");
ArchitectureSymbol a = symTab.<ArchitectureSymbol>resolve(
"Fixed_ThreeInputCNN_M14",
ArchitectureSymbol.KIND).orElse(null);
assertNotNull(a);
a.resolve();
a.getBody().getOutputTypes();
}*/
}
......@@ -24,8 +24,7 @@ import de.monticore.lang.monticar.cnnarch.AbstractSymtabTest;
import de.monticore.lang.monticar.cnnarch._ast.ASTArchitecture;
import de.monticore.lang.monticar.cnnarch._ast.ASTCNNArchNode;
import de.monticore.lang.monticar.cnnarch._cocos.CNNArchCoCoChecker;
import de.monticore.lang.monticar.cnnarch._cocos.CNNArchPostResolveCocos;
import de.monticore.lang.monticar.cnnarch._cocos.CNNArchPreResolveCocos;
import de.monticore.lang.monticar.cnnarch._cocos.CNNArchCocos;
import de.monticore.lang.monticar.cnnarch._symboltable.ArchitectureSymbol;
import de.monticore.symboltable.Scope;
import de.se_rwth.commons.logging.Finding;
......@@ -63,8 +62,8 @@ public class AbstractCoCoTest extends AbstractSymtabTest {
protected static void runCheckerWithSymTab(String modelPath, String model) {
Log.getFindings().clear();
runCocoCheck(CNNArchPreResolveCocos.createChecker(),
CNNArchPostResolveCocos.createChecker(),
runCocoCheck(CNNArchCocos.createPreResolveChecker(),
CNNArchCocos.createPostResolveChecker(),
modelPath,
model);
}
......@@ -76,8 +75,8 @@ public class AbstractCoCoTest extends AbstractSymtabTest {
protected static void checkValid(String modelPath, String model) {
Log.getFindings().clear();
runCocoCheck(
CNNArchPreResolveCocos.createChecker(),
CNNArchPostResolveCocos.createChecker(),
CNNArchCocos.createPreResolveChecker(),
CNNArchCocos.createPostResolveChecker(),
modelPath,
model);
new ExpectedErrorInfo().checkOnlyExpectedPresent(Log.getFindings());
......@@ -94,8 +93,8 @@ public class AbstractCoCoTest extends AbstractSymtabTest {
// check whether all the expected errors are present when using all cocos
Log.getFindings().clear();
runCocoCheck(
CNNArchPreResolveCocos.createChecker(),
CNNArchPostResolveCocos.createChecker(),
CNNArchCocos.createPreResolveChecker(),
CNNArchCocos.createPostResolveChecker(),
modelPath,
model);
expectedErrors.checkExpectedPresent(Log.getFindings(), "Got no findings when checking all "
......
......@@ -121,11 +121,14 @@ public class AllCoCoTest extends AbstractCoCoTest {
new CNNArchCoCoChecker(),
"invalid_tests", "IllegalName",
new ExpectedErrorInfo(2, ErrorCodes.ILLEGAL_NAME));
}
@Test
public void testInvalidPostResolveCocos(){
checkInvalid(new CNNArchCoCoChecker(),
new CNNArchCoCoChecker().addCoCo(new CheckArchitectureFinished()),
"invalid_tests", "UnfinishedArchitecture",
new ExpectedErrorInfo(1, ErrorCodes.UNFINISHED_ARCHITECTURE));
checkInvalid(new CNNArchCoCoChecker(),
new CNNArchCoCoChecker().addCoCo(new CheckLayerInputs()),
"invalid_tests", "InvalidInputShape",
......
......@@ -16,7 +16,7 @@ architecture ResNet34(img_height=224, img_width=224, img_channels=3, classes=100
conv(filter=3, channels=channels, stride=stride) ->
conv(filter=3, channels=channels, act=false)
|
skip(channels=channels, stride=stride, ?=(stride!=1))
skip(channels=channels, stride=stride, ? = (stride != 1))
) ->
Add() ->
Relu()
......
architecture UnfinishedArchitecture(inputs=10, classes=2){
def input Q(-oo:+oo)^{C:inputs} in
def output Q(0:1)^{C:classes} out
in ->
FullyConnected(units=64, no_bias=true) ->
Tanh() ->
(
FullyConnected(units=classes, no_bias=true) ->
Softmax() ->
out
|
)
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment