diff --git a/tools/odbclient/src/odbclient/odbclient.py b/tools/odbclient/src/odbclient/odbclient.py index 42dce924..8f37e65e 100644 --- a/tools/odbclient/src/odbclient/odbclient.py +++ b/tools/odbclient/src/odbclient/odbclient.py @@ -374,6 +374,88 @@ def variable(self, variable_name, instance_name, step_name, frame_id, nset_name= column_names = _ascii(_decode, labels) return pd.DataFrame(values, index=index, columns=column_names) + def history_regions(self, step_name): + """Query the history Regions of a given step. + + Parameters + ---------- + step_name : string + The name of the step + + Returns + ------- + historyRegions : list of strings + The name of history regions, which are in the required step. + """ + return self._query('get_history_regions', step_name) + + def history_outputs(self, step_name, history_region_name): + """Query the history Outputs of a given step in a given history region. + + Parameters + ---------- + step_name : string + The name of the step + + history_region_name: string + The name of the history region + + Returns + ------- + historyOutputs : list of strings + The name of the history outputs, which are in the required step and under the required history region + """ + hisoutputs = self._query("get_history_outputs", (step_name, history_region_name)) + + return hisoutputs + + + def history_output_values(self, step_name, history_region_name, historyoutput_name): + """Query the history Regions of a given step. + + Parameters + ---------- + step_name : string + The name of the step + + Returns + ------- + historyRegions : list of strings + The name of the step the history regions are in. + """ + hisoutput_valuesx, hisoutput_valuesy = self._query("get_history_output_values", (step_name, history_region_name, historyoutput_name)) + history_region_description = self._query("get_history_region_description", (step_name, history_region_name)) + historyoutput_data = pd.Series(hisoutput_valuesy, index = hisoutput_valuesx, name = history_region_description + ": " + historyoutput_name) + + return historyoutput_data + + def history_region_description(self, step_name, history_region_name): + """Query the description of a history Regions of a given step. + + Parameters + ---------- + step_name : string + The name of the step + history_region_name: string + The name of the history region + + Returns + ------- + historyRegion_description : list of strings + The description of the history region. + """ + history_region_description = self._query("get_history_region_description", (step_name, history_region_name)) + return history_region_description + + def history_info(self): + """Query all the information about the history outputs in a given odb. + Returns + ------- + dictionary : ldictionary which contains history information + """ + dictionary = _decode(self._query("get_history_info")) + return dictionary + def _query(self, command, args=None): args = _ascii(_encode, args) self._send_command(command, args) @@ -436,8 +518,13 @@ def _encode(arg): def _decode(arg): - return arg.decode('ascii') if isinstance(arg, bytes) else arg - + if isinstance(arg, bytes): + return arg.decode('ascii') + if isinstance(arg, dict): + return {_decode(key): _decode(value) for key, value in arg.items()} + if isinstance(arg, list): + return [_decode(element) for element in arg] + return arg def _guess_abaqus_bin(): if sys.platform == 'win32': @@ -455,7 +542,7 @@ def _guess_abaqus_bin_windows(): for guess in guesses: if os.path.exists(guess): return guess - return None + raise OSError("Could not guess abaqus binary path! Please submit as abaqus_bin parameter!") def _guess_pythonpath(python_env_path): diff --git a/tools/odbclient/tests/beam_3d_hex_quad.odb b/tools/odbclient/tests/beam_3d_hex_quad.odb index 33751ccd..2a2ff16e 100644 Binary files a/tools/odbclient/tests/beam_3d_hex_quad.odb and b/tools/odbclient/tests/beam_3d_hex_quad.odb differ diff --git a/tools/odbclient/tests/history_output_test.odb b/tools/odbclient/tests/history_output_test.odb new file mode 100644 index 00000000..36f306a7 Binary files /dev/null and b/tools/odbclient/tests/history_output_test.odb differ diff --git a/tools/odbclient/tests/test_odbclient.py b/tools/odbclient/tests/test_odbclient.py index 0eda5392..080113d3 100644 --- a/tools/odbclient/tests/test_odbclient.py +++ b/tools/odbclient/tests/test_odbclient.py @@ -22,6 +22,7 @@ import os import pytest +import json import numpy as np import pandas as pd @@ -49,6 +50,10 @@ def test_odbclient_instances(client): np.testing.assert_array_equal(client.instance_names(), ['PART-1-1']) +def test_odbclient_invalid_instance(client): + with pytest.raises(KeyError): + client.node_coordinates("FOO-1-1") + def test_odbclient_node_coordinates(client): expected = pd.read_csv('tests/node_coordinates.csv', index_col='node_id') pd.testing.assert_frame_equal(client.node_coordinates('PART-1-1'), expected) @@ -158,3 +163,55 @@ def test_variable_stress_integration_point(client): result = client.variable('S', 'PART-1-1', 'Load', 1, position='INTEGRATION POINTS') result.to_csv('tests/stress_integration_point.csv') pd.testing.assert_frame_equal(result, expected) + + +@pytest.fixture +def client_history(): + return odbclient.OdbClient('tests/history_output_test.odb') + + +def test_history_region_empty(client): + assert client.history_regions("Load") == ['Assembly ASSEMBLY'] + + +def test_history_region_non_empty(client_history): + assert client_history.history_regions("Step-1") == [ + 'Assembly ASSEMBLY', + 'Element ASSEMBLY.1', + 'Node ASSEMBLY.1', + 'Node ASSEMBLY.2', + ] + + +def test_history_outputs(client_history): + assert client_history.history_outputs("Step-1", 'Element ASSEMBLY.1') == [ + 'CTF1', + 'CTF2', + 'CTF3', + 'CTM1', + 'CTM2', + 'CTM3', + 'CU1', + 'CU2', + 'CU3', + 'CUR1', + 'CUR2', + 'CUR3', + ] + +def test_history_output_values(client_history): + assert client_history.history_output_values("Step-1", 'Element ASSEMBLY.1', 'CTF1').array[1] == pytest.approx(0.09999854117631912) + + +def test_history_region_description(client_history): + assert ( + client_history.history_region_description("Step-1", 'Element ASSEMBLY.1') + == "Output at assembly ASSEMBLY instance ASSEMBLY element 1" + ) + + +def test_history_info(client_history): + expected = json.loads(""" +{"Output at assembly ASSEMBLY instance ASSEMBLY node 1 region RP-1": {"History Outputs": ["RF1", "RF2", "RF3", "RM1", "RM2", "RM3", "U1", "U2", "U3", "UR1", "UR2", "UR3"], "History Region": "Node ASSEMBLY.1", "Steps ": ["Step-1", "Step-2"]}, "Output at assembly ASSEMBLY instance ASSEMBLY element 1": {"History Outputs": ["CTF1", "CTF2", "CTF3", "CTM1", "CTM2", "CTM3", "CU1", "CU2", "CU3", "CUR1", "CUR2", "CUR3"], "History Region": "Element ASSEMBLY.1", "Steps ": ["Step-1", "Step-2"]}, "Output at assembly ASSEMBLY instance ASSEMBLY node 2 region SET-5": {"History Outputs": ["RF1", "RF2", "RF3", "RM1", "RM2", "RM3", "U1", "U2", "U3", "UR1", "UR2", "UR3"], "History Region": "Node ASSEMBLY.2", "Steps ": ["Step-1", "Step-2"]}, "Output at assembly ASSEMBLY": {"History Outputs": ["ALLAE", "ALLCCDW", "ALLCCE", "ALLCCEN", "ALLCCET", "ALLCCSD", "ALLCCSDN", "ALLCCSDT", "ALLCD", "ALLDMD", "ALLDTI", "ALLEE", "ALLFD", "ALLIE", "ALLJD", "ALLKE", "ALLKL", "ALLPD", "ALLQB", "ALLSD", "ALLSE", "ALLVD", "ALLWK", "ETOTAL"], "History Region": "Assembly ASSEMBLY", "Steps ": ["Step-1", "Step-2"]}} + """) + assert client_history.history_info() == expected diff --git a/tools/odbserver/odbserver/__main__.py b/tools/odbserver/odbserver/__main__.py index dd42d109..0798f83a 100644 --- a/tools/odbserver/odbserver/__main__.py +++ b/tools/odbserver/odbserver/__main__.py @@ -41,7 +41,12 @@ def __init__(self, odbfile): "get_node_set": self.node_set, "get_element_set": self.element_set, "get_variable_names": self.variable_names, - "get_variable": self.variable + "get_variable": self.variable, + "get_history_regions": self.history_regions, + "get_history_outputs": self.history_outputs, + "get_history_output_values": self.history_output_values, + "get_history_region_description": self.history_region_description, + "get_history_info": self.history_info } def instances(self, _args): @@ -102,6 +107,30 @@ def variable(self, args): else: _send_response(variable) + def history_regions(self, step_name): + _send_response(self._odb.history_regions(str(step_name))) + + def history_outputs(self, args): + step_name, historyregion_name = args + step_name = str(step_name) + historyregion_name = str(historyregion_name) + _send_response(self._odb.history_outputs(step_name, historyregion_name)) + + def history_output_values(self, args): + step_name, historyregion_name, historyoutput_name = args + step_name = str(step_name) + historyregion_name = str(historyregion_name) + historyoutput_name = str(historyoutput_name) + _send_response(self._odb.history_output_values(step_name, historyregion_name, historyoutput_name)) + + def history_region_description(self, args): + step_name, historyregion_name = args + step_name = str(step_name) + historyregion_name = str(historyregion_name) + _send_response(self._odb.history_region_description(step_name, historyregion_name)) + + def history_info(self, args): + _send_response(self._odb.history_info()) def _send_response(pickle_data, numpy_arrays=None): numpy_arrays = numpy_arrays or [] diff --git a/tools/odbserver/odbserver/interface.py b/tools/odbserver/odbserver/interface.py index 8b59b2e3..f1a58eb9 100644 --- a/tools/odbserver/odbserver/interface.py +++ b/tools/odbserver/odbserver/interface.py @@ -20,6 +20,7 @@ import sys import numpy as np import odbAccess as ODB +import json class OdbInterface: @@ -27,7 +28,6 @@ class OdbInterface: def __init__(self, odbfile): self._odb = ODB.openOdb(odbfile) self._asm = self._odb.rootAssembly - self._index_cache = {} def instance_names(self): @@ -239,12 +239,189 @@ def index_block_data(block): def _instance_or_rootasm(self, instance_name): if instance_name == b'': - return self._asm + instance = self._asm + else: + instance = self._asm.instances[instance_name] + + element_types = {el.type for el in instance.elements} + unsupported_types = {et for et in element_types if et[0] != "C"} + if unsupported_types: + raise ValueError( + "Only continuum elements (C...) are supported at this point, sorry. " + "Please submit an issue to https://github.com/boschresearch/pylife/issues " + "if you need to support other types. " + "(Unsupported types %s found in instance %s)" % ( + ", ".join(unsupported_types), instance_name + )) + + return instance + + def history_regions(self, step_name): + """Get history regions, which belongs to the given step. + + Parameters + ---------- + step_name : Abaqus steps + It is always required. + + Returns + ------- + histRegions : history regions, which belong to the given step. In case of error it gives an error message. + It is a list of hist regions + """ try: - return self._asm.instances[instance_name] + + required_step = self._odb.steps[step_name] + histRegions = required_step.historyRegions.keys() + + return histRegions + + except Exception as e: + return e + + + def history_outputs(self, step_name, historyregion_name): + """Get history outputs, which belongs to the given step and history region. + + Parameters + ---------- + step_name : Abaqus steps + It is always required. + historyregion_name: Abaqus history region + It is always required. + + Returns + ------- + history_data : history data, which belong to the given step and history region. In case of error it gives an error message. + It is a list of history outputs. + + """ + try: + required_step = self._odb.steps[step_name] + + history_data = required_step.historyRegions[historyregion_name].historyOutputs.keys() + + return history_data + + except Exception as e: + return e + + + def history_output_values(self, step_name, historyregion_name, historyoutput_name): + """Get history output values, which belongs to the given step, history region and history output. + + Parameters + ---------- + step_name : Abaqus steps + It is always required. + historyregion_name: Abaqus history region + It is always required. + historyoutput_name: Abaqus history output + It is always required. + + Returns + ------- + x : time values of a history output. In case of error it gives an error message. + It is a list of data. + y : values of a history output. In case of error it gives an error message. + It is a list of data. + + """ + try: + required_step = self._odb.steps[step_name] + + history_data = required_step.historyRegions[historyregion_name].historyOutputs[historyoutput_name].data + + step_time = required_step.totalTime + + xdata = [] + ydata = [] + for ith in history_data: + xdata.append(ith[0]+step_time) + ydata.append(ith[1]) + + x = np.array(xdata) + y = np.array(ydata) + return x,y + except Exception as e: return e + def history_region_description(self, step_name, historyregion_name): + """Get history region description, which belongs to the given step and history region. + + Parameters + ---------- + step_name : Abaqus steps + It is always required. + historyregion_name: Abaqus history region + It is always required. + + Returns + ------- + history_description : description for history region, which is visible in Abaqus. In case of error it gives an error message. + It is string. + + """ + try: + required_step = self._odb.steps[step_name] + + history_description = required_step.historyRegions[historyregion_name].description + + return history_description + + except Exception as e: + return e + + + def history_info(self): + """Get steps, history regions, history outputs and write into a dictionary. + + + Returns + ------- + A dictionary which contains information about the history of a given odb file. + In case of error it gives an error message. + + """ + dict1 = {} + try: + steps = self._odb.steps.keys() + + for istep in steps: + regions = self.history_regions(step_name=istep) + + for iregion in regions: + + idescription = self.history_region_description(step_name= istep, historyregion_name= iregion) + + outputs = self.history_outputs(step_name= istep, historyregion_name= iregion) + + for ioutputs in outputs: + if "Repeated: key" in ioutputs: + outputs.remove(ioutputs) + + steplist = [] + for istep2 in steps: + try: + self._odb.steps[istep2].historyRegions[iregion].description + steplist.append(istep2) + except Exception as e: + continue + + dict5 = { + "History Region" : iregion, + "History Outputs" : outputs, + "Steps " : steplist + } + + dict1[idescription] = dict(dict5) + + return dict1 + except Exception as e: + return e + + def _set_position(field, user_request=None): """Translate string to symbolic constant and define default behavior.