Skip to content

Commit 5d7fa6a

Browse files
committed
Fix function expression evaluation
- Fixed the function expression evaluation. eval is unsafe, and ne.evaluate does not recognize the heaviside function -> convert to where / np.where.
1 parent 1064577 commit 5d7fa6a

File tree

6 files changed

+93
-79
lines changed

6 files changed

+93
-79
lines changed

src/fourc_webviewer/fourc_webserver.py

Lines changed: 22 additions & 23 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

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

src/fourc_webviewer/read_geometry_from_file.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@
44
"""
55

66
import re
7+
from pathlib import Path
78

89
import numpy as np
9-
from meshio.__about__ import __version__
10-
from meshio._common import warn
11-
from meshio._exceptions import ReadError
12-
from meshio._mesh import Mesh
1310
import pyvista as pv
1411
from fourcipp.fourc_input import FourCInput
1512
from lnmmeshio import read, write
16-
from lnmmeshio.meshio_to_discretization import mesh2Discretization
1713
from lnmmeshio.discretization import (
18-
PointNodeset,
1914
LineNodeset,
15+
PointNodeset,
2016
SurfaceNodeset,
2117
VolumeNodeset,
2218
)
2319
from lnmmeshio.fiber import Fiber
24-
from pathlib import Path
20+
from lnmmeshio.meshio_to_discretization import mesh2Discretization
21+
from meshio.__about__ import __version__
22+
from meshio._common import warn
23+
from meshio._exceptions import ReadError
24+
from meshio._mesh import Mesh
2525

2626
exodus_to_meshio_type = {
2727
"SPHERE": "vertex",
@@ -106,9 +106,8 @@
106106

107107

108108
def switch_node_order(mesh_exo: Mesh) -> Mesh:
109-
"""Switch node orders for read-in Exodus mesh such that the node
110-
orders are consistent upon conversion to lnmmeshio's Discretization
111-
object.
109+
"""Switch node orders for read-in Exodus mesh such that the node orders are
110+
consistent upon conversion to lnmmeshio's Discretization object.
112111
113112
Args:
114113
mesh_exo (meshio.Mesh): read-in Exodus mesh
@@ -120,7 +119,6 @@ def switch_node_order(mesh_exo: Mesh) -> Mesh:
120119

121120
# run through elements
122121
for cell_block in copy_mesh_exo.cells:
123-
124122
cell_type = cell_block.type
125123

126124
if cell_type in special_meshio_mesh_to_lnmmeshio_dis_to_vtu_node_order:
@@ -371,8 +369,10 @@ def read_exodus(filename, use_set_names=False): # noqa: C901
371369

372370

373371
def check_for_geometry_files(fourc_yaml: FourCInput) -> list:
374-
"""Checks the content of a fourc yaml file for referenced geometry files, e.g., contained within
375-
STRUCTURE GEOMETRY / FILE. Returns the identified file, whereby we previously verify whether all given files are the same file (required by 4C currently).
372+
"""Checks the content of a fourc yaml file for referenced geometry files,
373+
e.g., contained within STRUCTURE GEOMETRY / FILE. Returns the identified
374+
file, whereby we previously verify whether all given files are the same
375+
file (required by 4C currently).
376376
377377
Args:
378378
fourc_yaml(FourCInput): content of the fourc yaml file to be verified.
@@ -489,14 +489,16 @@ def get_element_ids_of_block(self, element_block_id):
489489
return np.where(self._dis.cell_data["GROUP_ID"] == element_block_id - 1)[0]
490490

491491
def get_all_nodes_in_element_block_exo(self, element_block_id: int):
492-
"""Retrieve all unique node indices in a specified element block for Exodus geometry.
492+
"""Retrieve all unique node indices in a specified element block for
493+
Exodus geometry.
494+
493495
Args:
494496
element_block_id (int): id of the element block to retrieve the nodes from
495497
Returns:
496498
ndarray: array of node indices within the specified element block.
497499
"""
498500

499-
# get cummulative element counts for each block
501+
# get cumulative element counts for each block
500502
cum_el_counts = np.cumsum([len(cs) for cs in self._mesh_exo.cells])
501503

502504
# get all element ids in the considered cell set
@@ -521,6 +523,8 @@ def get_all_nodes_in_element_block_exo(self, element_block_id: int):
521523
return np.unique(all_node_ids)
522524

523525
def enhance_exo_dis_with_fourc_yaml_info(self):
526+
"""Enhance contained Discretization with further information from the
527+
fourc yaml file -> read in nodesets, and material data."""
524528

525529
# --> read in nodeset info (pointnodesets, linenodesets, surfacenodesets, volumenodesets) based on the design sections specified in the yaml file
526530
# get all design sections
@@ -675,7 +679,9 @@ def convert_dis_to_vtu(self, override=True):
675679
)
676680

677681
def prepare_dis_for_vtu_output(self):
678-
"""Prepares discretization for vtu conversion by adding data contained within the yaml file (e.g. material id, design conditions) as nodal or element data."""
682+
"""Prepares discretization for vtu conversion by adding data contained
683+
within the yaml file (e.g. material id, design conditions) as nodal or
684+
element data."""
679685
self._dis.compute_ids(zero_based=False)
680686

681687
# write node data

src/fourc_webviewer_default_files/tutorial_contact_3d.4C.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ FUNCT1:
6464
TYPE: "linearinterpolation"
6565
NUMPOINTS: 3
6666
TIMES: [0, 0.1, 2]
67-
VALUES: [0,0.02,0.02]
67+
VALUES: [0, 0.02, 0.02]
6868
DESIGN SURF MORTAR CONTACT CONDITIONS 3D:
6969
- E: 1
7070
ENTITY_TYPE: node_set_id

0 commit comments

Comments
 (0)