Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] error during formula creation #1184

Open
wants to merge 4 commits into
base: v2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Docs/usage/stepsConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -780,8 +780,39 @@ Bestimmt den am häufigsten in einem Array vorkommenden Wert.
}
```

#### formula

Berechnung einer Formel. Hierbei kann man in der
Formel alle Grundrechenarten verwenden. Will man Berechnungen mit API-Daten in der Formel verwenden, kann man dies mit der normalen Syntax hierfür tun (`{key}`).

**Beispiel**

```JSON
{
"type": "calculate",
"action": "formula",
"formula": "{_req|data|number} * 8",
"decimal": 2,
"new_key": "Test"
}
```

`formula`:

[str](#string) - Formel die Berchnet werden soll.

`decimal`_(optional)_:

int - Nachkommastelle, auf die der Durchschnittswert gerundet werden soll.

#### Grundrechenarten

```warning::

Diese Funktionen sind veraltet und stadessen sollte `formula` verwendet werden.

```

Die Aktionen `multiply`, `divide`, `subtract` und `add` sind gleich aufgebaut. Daher haben die Keys auch die gleiche Bedeutung.
Als default ist der Wert zu `keys` immer auf der linken Seite der Gleichung. Alternativ: `keys_right`.

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ services:
- ./src/visuanalytics/out:/home/appuser/visuanalytics/out
- ./src/visuanalytics/instance:/home/appuser/visuanalytics/instance
- ./src/visuanalytics/resources:/home/appuser/visuanalytics/resources
- ./src/visuanalytics/resources/images:/home/appuser/visuanalytics/resources/images
16 changes: 15 additions & 1 deletion src/visuanalytics/analytics/transform/calculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import numbers
import operator
from functools import reduce
from tokenize import Number
import numpy as np

from visuanalytics.analytics.control.procedures.step_data import StepData
from visuanalytics.analytics.transform.util.key_utils import get_new_keys
from visuanalytics.analytics.transform.util.key_utils import get_new_key, get_new_keys
from visuanalytics.server.db import queries
from sympy.parsing.sympy_parser import parse_expr

CALCULATE_ACTIONS = {}
"""Ein Dictionary bestehend aus allen Calculate-Actions-Methoden."""
Expand Down Expand Up @@ -213,6 +215,18 @@ def _bi_calculate(values: dict, data: StepData, op):

data.insert_data(new_key, res, values)

@register_calculate
def calculate_formula(values: dict, data: StepData):
""""""
formula = data.format(values.get("formula", ""), values)

new_key = values["new_key"]
new_value = parse_expr(formula)

if isinstance(new_value, numbers.Number) and values.get("decimal", None):
new_value = round(new_value, data.get_data(values["decimal"], values, numbers.Number))

data.insert_data(new_key, new_value, values)

@register_calculate
def calculate_multiply(values: dict, data: StepData):
Expand Down
1 change: 1 addition & 0 deletions src/visuanalytics/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pyhumps==1.6.1
xmltodict==0.12.0
pydub==0.25.1
kiwisolver==1.0.1
sympy==1.9

# Server requirements
flask==1.1.2
Expand Down
11 changes: 3 additions & 8 deletions src/visuanalytics/server/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from visuanalytics.util.resources import TEMP_LOCATION, get_resource_path, get_temp_path
from visuanalytics.util.config_manager import get_private, set_private

from ast2json import str2json
from base64 import b64encode
from visuanalytics.analytics.apis.checkapi import check_api
from sympy.parsing.sympy_parser import parse_expr

logger = logging.getLogger()

Expand Down Expand Up @@ -617,16 +617,11 @@ def testformula():
tmp = queries.remove_toplevel_key(formula["formula"])
if tmp[0].isdigit:
tmp = "|" + tmp
str2json(tmp.replace("|", "uzjhnjtdryfguljkm"))
parse_expr(tmp.replace("|", "uzjhnjtdryfguljkm"))
return flask.jsonify({"accepted": True})

except SyntaxError:
return flask.jsonify({"accepted": False})

except Exception:
logger.exception("An error occurred: ")
err = flask.jsonify({"err_msg": "An error occurred while testing a formula"})
return err, 400
return flask.jsonify({"accepted": False})


@api.route("/scene", methods=["POST"])
Expand Down
13 changes: 8 additions & 5 deletions src/visuanalytics/server/db/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1498,10 +1498,14 @@ def _extend_formula_keys(obj, datasource_name, formula_keys):
float(part)
except Exception:
transformed_keys = [key if key not in part else part for key in transformed_keys]
if part != "" and part not in formula_keys and part not in transformed_keys:
if part != "" and part not in transformed_keys:
transformed_keys.append(part)
part_temp = remove_toplevel_key(part)
obj = obj.replace(part, "_req|" + datasource_name + "|" + part_temp)
if part not in formula_keys:
part_temp = remove_toplevel_key(part)

obj = obj.replace(part, f"{{_req|{datasource_name}|{part_temp}}}")
else:
obj = obj.replace(part, f"{{{part}}}")
return obj


Expand All @@ -1520,11 +1524,10 @@ def _insert_param_values(con, job_id, topic_values, config=True):

def _generate_transform(formulas, old_transform):
transform = []
counter = 0
for method in old_transform:
transform.append(method)
for formula in formulas:
transform_part, counter = generate_step_transform(formula["formelString"], formula["formelName"], counter, copy=formula.get("copy_key", None), array_key=formula.get("array_key", None), loop_key=formula.get("loop_key", ""), decimal=formula.get("decimal", 2))
transform_part = generate_step_transform(formula["formelString"], formula["formelName"], copy=formula.get("copy_key", None), array_key=formula.get("array_key", None), loop_key=formula.get("loop_key", ""), decimal=formula.get("decimal", 2))
if transform_part is None:
return None
transform += transform_part
Expand Down
134 changes: 25 additions & 109 deletions src/visuanalytics/util/infoprovider_utils.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,24 @@
import json
from ast2json import str2json

splitString = "uzjhnjtdryfguljkm"

import logging
from typing import Optional, Union

def get_transformations(tree, k, key_name, counter):
"""
Simuliert einen globalen Scope für die Methode, die die Liste der Transformtypen generiert.
from ast2json import str2json

:param tree: Dictionary mit der Formel repräsentiert als ein abstrakter Syntaxbaum
:type tree: dict
:param k: Key zu dem relevanten Part des Syntaxbaums (z.B. der linke oder rechte Operand)
:type k: str
:param key_name: falls es sich nicht um ein Ziwschenergebnis handelt, wird das Ergebnis unter diesem Namen abgespeichert, damit es später wiederverwendet werden kann
:type key_name: str
:param counter: Zählervariable für die Keys der Zwischenergebnisse
:type counter: int
logger = logging.getLogger()

:return: Liste der Transformtypen
"""
operations = {
"ADD": "add",
"SUB": "subtract",
"MUL": "multiply",
"DIV": "divide",
"MOD": "modulo"
}
splitString = "uzjhnjtdryfguljkm"

calc_template = {
"type": "calculate",
"action": "",
"decimal": 2,
"keys": ["_req"]
def get_transformation(formula: str, decimal: Optional[int], key_name: str):
calculation = {
"type": "calculate",
"action": "formula",
# TODO solve better (get formula without splitString)
"formula":formula.replace(splitString, "|"),
"decimal": decimal if decimal else 2,
"new_key": key_name
}

new_key_template = "_new_key_"

calculations = []

def build_calc_list(tree, k, counter, key_name=None):
"""
Erstellt rekursiv eine Liste von primitiven Rechenoperationen für den Transform-Schritt, mit der die Formeln
eines Infoproviders umgesetzt werden können.

:param tree: Dictionary mit der Formel repräsentiert als ein abstrakter Syntaxbaum
:type tree: dict
:param k: Key zu dem relevanten Part des Syntaxbaums (z.B. der linke oder rechte Operand)
:type k: str
:param counter: Zählervariable für die Keys der Zwischenergebnisse
:type counter: int
:param key_name: falls es sich nicht um ein Ziwschenergebnis handelt, wird das Ergebnis unter diesem Namen abgespeichert, damit es später wiederverwendet werden kann
:type key_name: str

:return: Key, an dem das Zwischenergebnis einer Operation zu finden ist
"""
current_calc = tree[k]
contains_calc = False
keys = ["lop", "rop"]
new_key = new_key_template + str(counter) if key_name is None else key_name

for key in keys:
if type(current_calc[key]) == dict:
counter += 1
current_calc[key], temporary = build_calc_list(current_calc, key, counter)

calculation = calc_template.copy()
calculation["action"] = operations[current_calc["operator"]]
calculation["decimal"] = current_calc["decimal"]

if type(current_calc["lop"]) == str:
calculation.update({"keys": [current_calc["lop"].replace(splitString, "|")]})
else:
calculation.update({"value_left": current_calc["lop"]})
if type(current_calc["rop"]) == str:
calculation.update({"keys_right": [current_calc["rop"].replace(splitString, "|")]})
else:
calculation.update({"value_right": current_calc["rop"]})
calculation.update({"new_keys": [new_key]})

calculations.append(calculation)
if k != "tree":
return new_key, counter
return None, counter

tmp, counter = build_calc_list(tree, k, counter, key_name=key_name)
return calculations, counter
return calculation


def parse_to_own_format(rep, decimal, loop_var=None):
Expand Down Expand Up @@ -129,7 +63,7 @@ def parse_string(calculation_string):
return None


def generate_step_transform(formula, key_name, counter, copy=None, array_key=None, loop_key="", decimal=2):
def generate_step_transform(formula, key_name, copy=None, array_key=None, loop_key="", decimal=2):
"""
Erstellt die Liste der Transformtypen für eine Formel, falls die Formel keinen syntaktischen Fehler hat.
Dabei kann die Formel auf einen einzelnen Key, oder ein ganzes Array angewendet werden.
Expand Down Expand Up @@ -164,33 +98,15 @@ def generate_step_transform(formula, key_name, counter, copy=None, array_key=Non
loop_var = loop_var.replace("|", "")
formula = formula.replace("array_var", loop_var).replace("|", splitString)

ast_rep = parse_string(formula if array_key else formula)
transformation = get_transformation(formula, decimal, key_name)

if not ast_rep:
return None, counter
if array_key:
result.append({
"type": "transform_array",
"array_key": copy if copy else array_key,
"transform": [transformation]
})
else:
parsed_result = parse_to_own_format(ast_rep["body"][0]["value"], decimal=decimal, loop_var=(loop_var if array_key else None))

if array_key:
transformations, counter = get_transformations({
"tree": parsed_result
}, "tree", key_name, counter) + [
{
"type": "calculate",
"action": "multiply",
"decimal": decimal,
"keys": [key_name],
"value_right": 1,
"new_keys": [loop_var]
}
]
return result + [{
"type": "transform_array",
"array_key": copy if copy else array_key,
"transform": transformations
}], counter
else:
transformations, counter = get_transformations({
"tree": parsed_result
}, "tree", key_name, counter)
return result + transformations, counter
result.append(transformation)

return result