Skip to content

Commit 3a6e5ad

Browse files
committed
Fix function expression evaluation and node order
- Fixed the function expression evaluation. eval is unsafe, and ne.evaluate does not recognize the heaviside function -> convert to where / np.where. - Fixed node ordering by shifting only the node indices and not their coordinates.
1 parent 1064577 commit 3a6e5ad

File tree

9 files changed

+100
-93
lines changed

9 files changed

+100
-93
lines changed

src/fourc_webviewer/fourc_webserver.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@
2323
from fourc_webviewer.input_file_utils.io_utils import (
2424
create_file_object_for_browser,
2525
get_master_and_linked_material_indices,
26+
get_variable_data_by_name_in_funct_item,
2627
read_fourc_yaml_file,
2728
write_fourc_yaml_file,
28-
get_variable_data_by_name_in_funct_item,
2929
)
3030
from fourc_webviewer.python_utils import convert_string2number, find_value_recursively
3131
from fourc_webviewer.read_geometry_from_file import (
3232
FourCGeometry,
3333
)
3434

35-
3635
# always set pyvista to plot off screen with Trame
3736
pv.OFF_SCREEN = True
3837

@@ -266,15 +265,15 @@ def update_pyvista_render_objects(self, init_rendering=False):
266265
)
267266

268267
# get coords of node with prescribed result description
269-
self._server_vars[
270-
"pv_selected_result_description_node_coords"
271-
] = self._server_vars["pv_mesh"].points[
272-
self.state.result_description_section[
273-
self.state.selected_result_description_id
274-
]["PARAMETERS"]["NODE"]
275-
- 1,
276-
:,
277-
]
268+
self._server_vars["pv_selected_result_description_node_coords"] = (
269+
self._server_vars["pv_mesh"].points[
270+
self.state.result_description_section[
271+
self.state.selected_result_description_id
272+
]["PARAMETERS"]["NODE"]
273+
- 1,
274+
:,
275+
]
276+
)
278277

279278
# update plotter / rendering
280279
pv_render.update_pv_plotter(
@@ -352,9 +351,9 @@ def init_general_sections_state_and_server_vars(self):
352351
self.state.general_sections[main_section_name] = {}
353352

354353
# add subsection
355-
self.state.general_sections[main_section_name][
356-
section_name
357-
] = section_data
354+
self.state.general_sections[main_section_name][section_name] = (
355+
section_data
356+
)
358357

359358
def sync_general_sections_from_state(self):
360359
"""Syncs the server-side general sections based on the current values
@@ -436,9 +435,9 @@ def init_materials_state_and_server_vars(self):
436435
):
437436
if mat_id in linked_material_indices_item:
438437
# add linked material indices
439-
mat_item_val["RELATIONSHIPS"][
440-
"LINKED MATERIALS"
441-
] = linked_material_indices_item
438+
mat_item_val["RELATIONSHIPS"]["LINKED MATERIALS"] = (
439+
linked_material_indices_item
440+
)
442441

443442
# add master material index
444443
mat_item_val["RELATIONSHIPS"]["MASTER MATERIAL"] = material_indices[
@@ -498,9 +497,9 @@ def sync_materials_sections_from_state(self):
498497
# write to server-side content
499498
self._server_vars["fourc_yaml_content"]["MATERIALS"] = new_materials_section
500499
if new_cloning_material_map_section:
501-
self._server_vars["fourc_yaml_content"][
502-
"CLONING MATERIAL MAP"
503-
] = new_cloning_material_map_section
500+
self._server_vars["fourc_yaml_content"]["CLONING MATERIAL MAP"] = (
501+
new_cloning_material_map_section
502+
)
504503

505504
def init_design_conditions_state_and_server_vars(self):
506505
"""Initialize the state and server variables for the design condition
@@ -692,9 +691,9 @@ def sync_result_description_section_from_state(self):
692691
new_result_description_section.append({field: params})
693692

694693
# set result description section on the server
695-
self._server_vars["fourc_yaml_content"][
696-
"RESULT DESCRIPTION"
697-
] = new_result_description_section
694+
self._server_vars["fourc_yaml_content"]["RESULT DESCRIPTION"] = (
695+
new_result_description_section
696+
)
698697

699698
def init_funct_state_and_server_vars(self):
700699
"""Initialize the state and server variables for the function
@@ -1128,7 +1127,6 @@ def click_convert_button(self, **kwargs):
11281127

11291128
with open(temp_fourc_yaml_file, "w") as f:
11301129
f.write(self.state.fourc_yaml_file["content"].decode("utf-8"))
1131-
# copy eventual exodus file as well
11321130

11331131
if self._server_vars["fourc_yaml_read_in_status"]:
11341132
self.state.read_in_status = self.state.all_read_in_statuses["success"]

src/fourc_webviewer/gui_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ def _functions_panel(server):
540540
display_mode_bar="true",
541541
)
542542
server.controller.figure_update = figure.update
543-
if server.state.funct_section[server.state.selected_funct][
543+
if server.state.funct_section[
544+
server.state.selected_funct
545+
][
544546
server.state.selected_funct_item
545547
][
546548
"VISUALIZATION"

src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
import numexpr as ne
99
import numpy as np
1010
import plotly.express as px
11+
1112
from fourc_webviewer.input_file_utils.io_utils import (
1213
get_variable_data_by_name_in_funct_item,
1314
)
1415

15-
1616
# functional expressions / constants known by 4C, that are replaced by the numpy counterpart during evaluation
1717
DEF_FUNCT = ["exp", "sqrt", "log", "sin", "cos", "tan", "heaviside", "pi"]
1818

1919

2020
def get_variable_names_in_funct_expression(funct_expression: str):
21-
"""Returns all variable names present in a functional expression, using regular expressions."""
21+
"""Returns all variable names present in a functional expression, using
22+
regular expressions."""
2223
vars_found = re.findall(r"[A-Za-z_]+", funct_expression)
2324
return [
2425
v for v in vars_found if v not in DEF_FUNCT and v not in ["t", "x", "y", "z"]
@@ -159,48 +160,54 @@ def funct_using_eval(x, y, z, t):
159160
Returns:
160161
parsed object using ast.literal_eval
161162
"""
162-
# funct_string copy
163+
# Create a safe environment
164+
safe_dict = {
165+
"x": x,
166+
"y": y,
167+
"z": z,
168+
"t": t,
169+
"sin": np.sin,
170+
"cos": np.cos,
171+
"exp": np.exp,
172+
"log": np.log,
173+
"sqrt": np.sqrt,
174+
"where": np.where,
175+
"pi": np.pi,
176+
# "heaviside": np.heaviside, -> no heaviside, numexpr will
177+
# not deal with this
178+
# add other safe functions as needed
179+
}
180+
163181
funct_string_copy = funct_string
164182

165-
# replace variables by their functional expressions
183+
# replace variables by their
166184
for k, v in variable_funct_strings.items():
167185
funct_string_copy = re.sub(
168186
rf"(?<![A-Za-z]){k}(?![A-Za-z])", v, funct_string_copy
169187
)
170188

171-
# replace the defined functions in the funct_string with "<DEF_FUNCT>"
172-
for i in range(len(DEF_FUNCT)):
173-
funct_string_copy = funct_string_copy.replace(
174-
DEF_FUNCT[i], f"np.{DEF_FUNCT[i]}"
175-
)
176-
177-
# replace the used power sign
189+
# replace heaviside functions with where / np.where
178190
funct_string_copy = funct_string_copy.replace("^", "**")
179-
180-
# replace variables
181-
funct_string_copy = (
182-
funct_string_copy.replace("x", str(x))
183-
.replace("y", str(y))
184-
.replace("z", str(z))
185-
.replace("t", str(t))
191+
funct_string_copy = re.sub(
192+
r"heaviside\(([^),]+)\)", r"where(\1 >= 0, 1, 0)", funct_string_copy
186193
)
187-
188-
# for heaviside: np.heaviside takes two arguments -> second argument denotes the function value at the first argument -> we set it by default to 0
189194
funct_string_copy = re.sub(
190-
r"heaviside\((.*?)\)", r"heaviside(\1,0)", funct_string_copy
191-
) # usage of raw strings, (.*?) is a non greedy capturing, and \1 replaces the captured value
195+
r"heaviside\(([^),]+),\s*([^)]+)\)",
196+
r"where(\1 > 0, 1, where(\1 == 0, \2, 0))",
197+
funct_string_copy,
198+
)
192199

193-
return eval(
194-
funct_string_copy, {"np": np}, {}
195-
) # this parses string in as a function
200+
# Numexpr evaluation (much safer)
201+
return ne.evaluate(funct_string_copy, local_dict=safe_dict)
196202

197203
return np.frompyfunc(funct_using_eval, 4, 1)
198204

199205

200206
def construct_funct_string_from_variable_data(
201207
variable_name: str, funct_section_item: dict
202208
):
203-
"""Constructs a functional string from the given data for a function variable."""
209+
"""Constructs a functional string from the given data for a function
210+
variable."""
204211

205212
# retrieve variable data
206213
variable_data = get_variable_data_by_name_in_funct_item(
@@ -212,8 +219,9 @@ def construct_funct_string_from_variable_data(
212219
match variable_data["TYPE"]:
213220
case "linearinterpolation":
214221
# get times and values
215-
times, values = np.array(variable_data["TIMES"]), np.array(
216-
variable_data["VALUES"]
222+
times, values = (
223+
np.array(variable_data["TIMES"]),
224+
np.array(variable_data["VALUES"]),
217225
)
218226

219227
# consistency check: time should start with 0.0
@@ -226,7 +234,7 @@ def construct_funct_string_from_variable_data(
226234
if time_instant_index != 0:
227235
funct_string += "+"
228236

229-
funct_string += f"({values[time_instant_index]}+({values[time_instant_index+1]}-{values[time_instant_index]})/({times[time_instant_index+1]}-{time_instant})*(t-{time_instant}))*heaviside(t-{time_instant})*heaviside({times[time_instant_index+1]}-t)"
237+
funct_string += f"({values[time_instant_index]}+({values[time_instant_index + 1]}-{values[time_instant_index]})/({times[time_instant_index + 1]}-{time_instant})*(t-{time_instant}))*heaviside(t-{time_instant})*heaviside({times[time_instant_index + 1]}-t)"
230238

231239
funct_string += ")"
232240

@@ -247,7 +255,7 @@ def construct_funct_string_from_variable_data(
247255
if time_instant_index != 0:
248256
funct_string += "+"
249257

250-
funct_string += f"({descriptions[time_instant_index]}*heaviside(t-{time_instant})*heaviside({times[time_instant_index+1]}-t))"
258+
funct_string += f"({descriptions[time_instant_index]}*heaviside(t-{time_instant})*heaviside({times[time_instant_index + 1]}-t))"
251259

252260
funct_string += ")"
253261

src/fourc_webviewer/input_file_utils/io_utils.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,7 @@ def get_main_and_clustered_section_names(sections_list):
192192
# append the main section "FUNCTIONS"
193193
main_sections.append("FUNCTIONS")
194194

195-
clustered_sections_to_be_added = (
196-
[]
197-
) # list of clustered sections to be added
195+
clustered_sections_to_be_added = [] # list of clustered sections to be added
198196
# add current element to clustered sections and remove it from sections
199197
clustered_sections_to_be_added.append(sections.pop(0))
200198

@@ -315,9 +313,7 @@ def get_master_and_linked_material_indices(materials_section):
315313
master_mat_indices = [mat_item["MAT"] for mat_item in materials_section]
316314

317315
# check whether some of the master materials are actually related to others, and eliminate them from the master material index list
318-
linked_mat_indices = (
319-
[]
320-
) # list of linked material indices for each of the "master" materials: [<list of linked materials for "MASTER" 1>, <list of linked materials for "MASTER" 2>,... ]
316+
linked_mat_indices = [] # list of linked material indices for each of the "master" materials: [<list of linked materials for "MASTER" 1>, <list of linked materials for "MASTER" 2>,... ]
321317
for master_mat_index in master_mat_indices:
322318
linked_mat_indices.append(
323319
find_linked_materials(
@@ -339,7 +335,9 @@ def get_master_and_linked_material_indices(materials_section):
339335
del master_mat_indices[next_master_list_ind]
340336
del linked_mat_indices[next_master_list_ind]
341337
# next_master_ind stays the same
342-
elif set(linked_mat_indices[curr_master_list_ind]).issubset(
338+
elif set(
339+
linked_mat_indices[curr_master_list_ind]
340+
).issubset(
343341
set(linked_mat_indices[next_master_list_ind])
344342
): # this means that the current element is not truly a master -> has to be eliminated and we also break out of the for-loop
345343
del master_mat_indices[curr_master_list_ind]
@@ -359,7 +357,8 @@ def get_master_and_linked_material_indices(materials_section):
359357
def get_variable_data_by_name_in_funct_item(
360358
funct_section_item: dict, variable_name: str
361359
):
362-
"""Retrieves the entire dictionary for the variable called <variable_name> from the specified function section item, e.g. FUNCT1.
360+
"""Retrieves the entire dictionary for the variable called <variable_name>
361+
from the specified function section item, e.g. FUNCT1.
363362
364363
Args:
365364
funct_section_item (dict): specified function item as a

0 commit comments

Comments
 (0)