diff --git a/pyansys/_version.py b/pyansys/_version.py index f9c55e5352..decc211331 100644 --- a/pyansys/_version.py +++ b/pyansys/_version.py @@ -1,5 +1,5 @@ # major, minor, patch -version_info = 0, 43, 1 +version_info = 0, 43, 2 # Nice string for the version __version__ = '.'.join(map(str, version_info)) diff --git a/pyansys/launcher.py b/pyansys/launcher.py index f2b5cd7e6d..339067c90d 100644 --- a/pyansys/launcher.py +++ b/pyansys/launcher.py @@ -1,14 +1,11 @@ """Module for launching MAPDL locally.""" from glob import glob -import time -import subprocess import re import warnings import os import appdirs import tempfile import socket -import pexpect from pyansys.misc import is_float, random_string from pyansys.errors import LockFileException, VersionError @@ -209,114 +206,6 @@ def check_lock_file(path, jobname, override): 'running at "%s"' % path) - -def launch_corba(exec_file=None, run_location=None, jobname=None, nproc=None, - additional_switches='', start_timeout=60): - """Start MAPDL in AAS mode - - - Notes - ----- - The CORBA interface is likely to fail on computers with multiple - network adapters. The ANSYS RPC isn't smart enough to determine - the right adapter and will likely try to communicate on the wrong - IP. - """ - # Using stored parameters so launch command can be run from a - # cached state (when launching the GUI) - - # can't run /BATCH in windows, so we trick it using "-b" and - # provide a dummy input file - if os.name == 'nt': - # must be a random filename to avoid conflicts with other - # potential instances - tmp_file = '%s.inp' % random_string(10) - with open(os.path.join(run_location, tmp_file), 'w') as f: - f.write('FINISH') - additional_switches += ' -b -i %s -o out.txt' % tmp_file - - # command must include "aas" flag to start MAPDL server - command = '"%s" -aas -j %s -np %d %s' % (exec_file, - jobname, - nproc, - additional_switches) - - # if os.name == 'nt': # required after v190 - # command = 'START /B "MAPDL" %s' % command - - # remove any broadcast files - broadcast_file = os.path.join(run_location, 'mapdl_broadcasts.txt') - if os.path.isfile(broadcast_file): - os.remove(broadcast_file) - - # windows 7 won't run this - # if os.name == 'nt' and platform.release() == '7': - # cwd = os.getcwd() - # os.chdir(run_location) - # os.system('START /B "MAPDL" %s' % command) - # os.chdir(cwd) - # else: - subprocess.Popen(command, shell=True, - cwd=run_location, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) - - # listen for broadcast file - telapsed = 0 - tstart = time.time() - started_rpc = False - while telapsed < start_timeout and not started_rpc: - try: - if os.path.isfile(broadcast_file): - broadcast = open(broadcast_file).read() - # see if connection to RPC has been made - rpc_txt = 'visited:collaborativecosolverunitior-set:' - started_rpc = rpc_txt in broadcast - - time.sleep(0.1) - telapsed = time.time() - tstart - - except KeyboardInterrupt: - raise KeyboardInterrupt - - # exit if timed out - if not started_rpc: - err_str = 'Unable to start ANSYS within %.1f seconds' % start_timeout - if os.path.isfile(broadcast_file): - broadcast = open(broadcast_file).read() - err_str += '\n\nLast broadcast:\n%s' % broadcast - raise TimeoutError(err_str) - - # return CORBA key - keyfile = os.path.join(run_location, 'aaS_MapdlId.txt') - return open(keyfile).read() - - -def launch_pexpect(exec_file=None, run_location=None, jobname=None, nproc=None, - additional_switches='', start_timeout=60): - """Launch MAPDL as a pexpect process. - - Limited to only a linux instance - """ - command = '%s -j %s -np %d %s' % (exec_file, jobname, nproc, - additional_switches) - process = pexpect.spawn(command, cwd=run_location) - process.delaybeforesend = None - - try: - index = process.expect(['BEGIN:', 'CONTINUE'], - timeout=start_timeout) - except: # capture failure - raise RuntimeError(process.before.decode('utf-8')) - - if index: # received ... press enter to continue - process.sendline('') - process.expect('BEGIN:', timeout=start_timeout) - - return process - - def launch_mapdl(exec_file=None, run_location=None, mode=None, jobname='file', nproc=2, override=False, loglevel='INFO', additional_switches='', start_timeout=120, @@ -632,16 +521,12 @@ def launch_mapdl(exec_file=None, run_location=None, mode=None, jobname='file', if mode == 'console': from pyansys.mapdl_console import MapdlConsole - process = launch_pexpect(**start_parm) - return MapdlConsole(process, loglevel=loglevel, - log_apdl=log_apdl, **start_parm) + return MapdlConsole(loglevel=loglevel, log_apdl=log_apdl, **start_parm) elif mode == 'corba': from pyansys.mapdl_corba import MapdlCorba - corba_key = launch_corba(**start_parm) - return MapdlCorba(corba_key, loglevel=loglevel, - log_apdl=log_apdl, - log_broadcast=kwargs.get('log_broadcast', False), - **start_parm) + return MapdlCorba(loglevel=loglevel, log_apdl=log_apdl, + log_broadcast=kwargs.get('log_broadcast', + False), **start_parm) else: raise ValueError('Invalid mode %s' % mode) @@ -663,12 +548,6 @@ def check_mode(mode, version): if mode == 'corba': if version < 170: raise VersionError('CORBA AAS mode requires MAPDL v17.0 or newer.') - # elif version > 20.1 and os.name == 'nt': - # raise ValueError('CORBA AAS mode on Windows requires MAPDL 2020R1' - # ' or earlier.') - # elif version >= 20.0 and os.name == 'posix': - # raise ValueError('CORBA AAS mode on Linux requires MAPDL 2019R3' - # ' or earlier.') elif mode == 'console' and is_win: raise ValueError('Console mode requires Linux') diff --git a/pyansys/mapdl.py b/pyansys/mapdl.py index 73b65d89fc..2a0e302e44 100644 --- a/pyansys/mapdl.py +++ b/pyansys/mapdl.py @@ -19,7 +19,7 @@ from pyansys.element_commands import element_commands from pyansys.errors import MapdlRuntimeError, MapdlInvalidRoutineError from pyansys.plotting import general_plotter - +from pyansys.launcher import get_ansys_path MATPLOTLIB_LOADED = True try: @@ -459,22 +459,24 @@ def open_gui(self, include_result=True): # pragma: no cover if os.path.isfile(tmp_database): os.remove(tmp_database) - # get the state, close, and finish + # cache result file, version, and routine before closing + resultfile = self._result_file + version = self.version prior_processor = self.parameters.routine + + # get the close, and finish self.finish() self.save(tmp_database) - # self.exit(close_log=False) self.exit() # copy result file to temp directory if include_result: - resultfile = os.path.join(self.path, '%s.rst' % self.jobname) if os.path.isfile(resultfile): tmp_resultfile = os.path.join(save_path, '%s.rst' % name) copyfile(resultfile, tmp_resultfile) # write temporary input file - start_file = os.path.join(save_path, 'start%s.ans' % self.version) + start_file = os.path.join(save_path, 'start%s.ans' % version) with open(start_file, 'w') as f: f.write('RESUME\n') @@ -486,22 +488,25 @@ def open_gui(self, include_result=True): # pragma: no cover # issue system command to run ansys in GUI mode cwd = os.getcwd() os.chdir(save_path) - os.system('cd "%s" && "%s" -g -j %s' % (save_path, - self._start_parm['exec_file'], - name)) + exec_file = self._start_parm.get('exec_file', + get_ansys_path(allow_input=False)) + os.system('cd "%s" && "%s" -g -j %s' % (save_path, exec_file, name)) os.chdir(cwd) # must remove the start file when finished os.remove(start_file) os.remove(other_start_file) - # reload database when finished - # self._launch() # TODO + # reattach to a new session and reload database + self._launch(self._start_parm) self.resume(tmp_database) if prior_processor is not None: if 'BEGIN' not in prior_processor: self.run('/%s' % prior_processor) + def _launch(self, *args, **kwargs): # pragma: no cover + raise NotImplementedError('Implemented by child class') + def _close_apdl_log(self): """Closes the APDL log""" if self._apdl_log is not None: @@ -1047,6 +1052,20 @@ def result(self): if not self._local: raise RuntimeError('Binary interface only available when result is local.') + result_path = self._result_file + if not os.path.isfile(result_path): + raise FileNotFoundError('No results found at %s' % result_path) + return pyansys.read_binary(result_path) + + @property + def _result_file(self): + """Path of the result file + + Notes + ----- + There may be multiple result files at this location (if not + combining results) + """ try: result_path = self.inquire('RSTFILE') except RuntimeError: @@ -1056,12 +1075,7 @@ def result(self): result_path = os.path.join(self.path, '%s.rst' % self._jobname) elif not os.path.dirname(result_path): result_path = os.path.join(self.path, '%s.rst' % result_path) - - # there may be multiple result files at this location (if not - # combining results) - if not os.path.isfile(result_path): - raise FileNotFoundError('No results found at %s' % result_path) - return pyansys.read_binary(result_path) + return result_path def _get(self, *args, **kwargs): """Simply use the default get method""" @@ -1332,7 +1346,7 @@ def read_float_from_inline_function(self, function_str): Returns ------- - float + value : float Value returned by inline function. Examples diff --git a/pyansys/mapdl_console.py b/pyansys/mapdl_console.py index 05e7db01f6..ec3b74cc9c 100644 --- a/pyansys/mapdl_console.py +++ b/pyansys/mapdl_console.py @@ -2,6 +2,7 @@ Used when launching Mapdl via pexpect on Linux when <= 17.0 """ +import pexpect import time import re @@ -41,26 +42,50 @@ ignored = re.compile(r'[\s\S]+'.join(['WARNING', 'command', 'ignored'])) +def launch_pexpect(exec_file=None, run_location=None, jobname=None, nproc=None, + additional_switches='', start_timeout=60): + """Launch MAPDL as a pexpect process. + + Limited to only a linux instance + """ + command = '%s -j %s -np %d %s' % (exec_file, jobname, nproc, + additional_switches) + process = pexpect.spawn(command, cwd=run_location) + process.delaybeforesend = None + + try: + index = process.expect(['BEGIN:', 'CONTINUE'], + timeout=start_timeout) + except: # capture failure + raise RuntimeError(process.before.decode('utf-8')) + + if index: # received ... press enter to continue + process.sendline('') + process.expect('BEGIN:', timeout=start_timeout) + + return process + + class MapdlConsole(_MapdlCore): """Control interaction with an ANSYS shell instance. Only works on Linux. - - Parameters - ---------- - process : pexpect - """ - def __init__(self, process, loglevel='INFO', log_apdl='w', - use_vtk=True, **start_parm): + def __init__(self, loglevel='INFO', log_apdl='w', use_vtk=True, + **start_parm): """Opens an ANSYS process using pexpect""" self._auto_continue = True self._continue_on_error = False - self._process = process + self._process = None + self._launch(start_parm) super().__init__(loglevel=loglevel, use_vtk=use_vtk, log_apdl=log_apdl, **start_parm) + def _launch(self, start_parm): + """Connect to MAPDL process using pexpect""" + self._process = launch_pexpect(**start_parm) + def _run(self, command): """Sends command and returns ANSYS's response""" self._reset_cache() diff --git a/pyansys/mapdl_corba.py b/pyansys/mapdl_corba.py index 2fecd9816f..3342e7371f 100644 --- a/pyansys/mapdl_corba.py +++ b/pyansys/mapdl_corba.py @@ -1,11 +1,12 @@ """CORBA implementation of the MAPDL interface""" import atexit +import subprocess import time import re import os from pyansys.mapdl import _MapdlCore -from pyansys.misc import threaded +from pyansys.misc import threaded, random_string from pyansys.errors import MapdlRuntimeError, MapdlExitedError try: @@ -47,6 +48,79 @@ def tail(filename, nlines): return qfile.read() +def launch_corba(exec_file=None, run_location=None, jobname=None, nproc=None, + additional_switches='', start_timeout=60): + """Start MAPDL in AAS mode + + Notes + ----- + The CORBA interface is likely to fail on computers with multiple + network adapters. The ANSYS RPC isn't smart enough to determine + the right adapter and will likely try to communicate on the wrong + IP. + """ + # Using stored parameters so launch command can be run from a + # cached state (when launching the GUI) + + # can't run /BATCH in windows, so we trick it using "-b" and + # provide a dummy input file + if os.name == 'nt': + # must be a random filename to avoid conflicts with other + # potential instances + tmp_file = '%s.inp' % random_string(10) + with open(os.path.join(run_location, tmp_file), 'w') as f: + f.write('FINISH') + additional_switches += ' -b -i %s -o out.txt' % tmp_file + + # command must include "aas" flag to start MAPDL server + command = '"%s" -aas -j %s -np %d %s' % (exec_file, + jobname, + nproc, + additional_switches) + + # remove any broadcast files + broadcast_file = os.path.join(run_location, 'mapdl_broadcasts.txt') + if os.path.isfile(broadcast_file): + os.remove(broadcast_file) + + # windows 7 won't run this + subprocess.Popen(command, shell=True, + cwd=run_location, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + # listen for broadcast file + telapsed = 0 + tstart = time.time() + started_rpc = False + while telapsed < start_timeout and not started_rpc: + try: + if os.path.isfile(broadcast_file): + broadcast = open(broadcast_file).read() + # see if connection to RPC has been made + rpc_txt = 'visited:collaborativecosolverunitior-set:' + started_rpc = rpc_txt in broadcast + + time.sleep(0.1) + telapsed = time.time() - tstart + + except KeyboardInterrupt: + raise KeyboardInterrupt + + # exit if timed out + if not started_rpc: + err_str = 'Unable to start ANSYS within %.1f seconds' % start_timeout + if os.path.isfile(broadcast_file): + broadcast = open(broadcast_file).read() + err_str += '\n\nLast broadcast:\n%s' % broadcast + raise TimeoutError(err_str) + + # return CORBA key + keyfile = os.path.join(run_location, 'aaS_MapdlId.txt') + return open(keyfile).read() + + class MapdlCorba(_MapdlCore): """CORBA implementation of the MAPDL interface @@ -64,14 +138,21 @@ class MapdlCorba(_MapdlCore): """ - def __init__(self, corba_key, loglevel='INFO', log_apdl='w', - use_vtk=True, log_broadcast=False, **start_parm): + def __init__(self, loglevel='INFO', log_apdl='w', use_vtk=True, + log_broadcast=False, **start_parm): """Open a connection to MAPDL via a CORBA interface""" super().__init__(loglevel=loglevel, use_vtk=use_vtk, log_apdl=log_apdl, log_broadcast=False, **start_parm) self._broadcast_logger = None self._server = None self._outfile = None + self._log_broadcast = log_broadcast + self._launch(start_parm) + super().__init__(loglevel=loglevel, use_vtk=use_vtk, log_apdl=log_apdl, + **start_parm) + + def _launch(self, start_parm): + corba_key = launch_corba(**start_parm) orb = CORBA.ORB_init() self._server = orb.string_to_object(corba_key) @@ -90,7 +171,7 @@ def __init__(self, corba_key, loglevel='INFO', log_apdl='w', corba_key) # separate logger for broadcast file - if log_broadcast: + if self._log_broadcast: self._broadcast_logger = self._start_broadcast_logger() INSTANCES.append(self) diff --git a/pyansys/mapdl_geometry.py b/pyansys/mapdl_geometry.py index 46dd097ed7..b23d929d93 100644 --- a/pyansys/mapdl_geometry.py +++ b/pyansys/mapdl_geometry.py @@ -125,10 +125,14 @@ def generate_surface(self, density=5, amin=None, amax=None, ninc=None): # duplicate areas to avoid affecting existing areas a_num = int(self._mapdl.get(entity='AREA', item1='NUM', it1num='MAXD')) - self._mapdl.numstr('AREA', a_num) - self._mapdl.agen(2, 'ALL', noelem=1) + with self._mapdl.chain_commands: + self._mapdl.numstr('AREA', a_num) + self._mapdl.agen(2, 'ALL', noelem=1) a_max = int(self._mapdl.get(entity='AREA', item1='NUM', it1num='MAXD')) - self._mapdl.asel('S', 'AREA', vmin=a_num + 1, vmax=a_max) + + with self._mapdl.chain_commands: + self._mapdl.asel('S', 'AREA', vmin=a_num + 1, vmax=a_max) + self._mapdl.aatt() # necessary to reset element/area meshing association # create a temporary etype etype_max = int(self._mapdl.get(entity='ETYP', item1='NUM', it1num='MAX')) diff --git a/pyansys/mesh.py b/pyansys/mesh.py index aad9c89cef..4dfdf811e4 100644 --- a/pyansys/mesh.py +++ b/pyansys/mesh.py @@ -34,6 +34,7 @@ def __init__(self, nnum=None, nodes=None, elem=None, elem_comps={}, rdat=[], rnum=[], keyopt={}): self._etype = None # internal element type reference self._grid = None # VTK grid + self._surf_cache = None # cached external surface self._enum = None # cached element numbering self._etype_cache = None # cached ansys element type numbering self._rcon = None # cached ansys element real constant @@ -58,6 +59,13 @@ def __init__(self, nnum=None, nodes=None, elem=None, self._rnum = rnum self._keyopt = keyopt + @property + def _surf(self): + """External surface""" + if self._surf_cache is None: + self._surf_cache = self._grid.extract_surface() + return self._surf_cache + @property def _has_nodes(self): """Returns True when has nodes""" diff --git a/pyansys/plotting.py b/pyansys/plotting.py index fe1a2e88f2..3b28aca8c6 100644 --- a/pyansys/plotting.py +++ b/pyansys/plotting.py @@ -11,6 +11,7 @@ def general_plotter(title, meshes, points, labels, background=None, off_screen=None, screenshot=False, window_size=None, + notebook=None, # add_mesh kwargs: color='w', show_edges=None, edge_color=None, point_size=5.0, @@ -47,6 +48,11 @@ def general_plotter(title, meshes, points, labels, window_size : list, optional Window size in pixels. Defaults to ``[1024, 768]`` + notebook : bool, optional + When True, the resulting plot is placed inline a jupyter + notebook. Assumes a jupyter console is active. Automatically + enables off_screen. + show_bounds : bool, optional Shows mesh bounds when ``True``. @@ -116,7 +122,10 @@ def general_plotter(title, meshes, points, labels, Smoothly render curved surfaces when plotting. Not helpful for all meshes. """ - pl = pv.Plotter(off_screen=off_screen) + if notebook: + off_screen = True + + pl = pv.Plotter(off_screen=off_screen, notebook=notebook) if background: pl.set_background(background) diff --git a/tests/test_cyclic.py b/tests/test_cyclic.py index 1c0f4f1dc5..c39f5bf928 100644 --- a/tests/test_cyclic.py +++ b/tests/test_cyclic.py @@ -109,14 +109,14 @@ def test_non_cyclic(): @pytest.mark.skipif(result_z is None, reason="Requires result file") def test_plot_sectors(tmpdir): filename = str(tmpdir.mkdir("tmpdir").join('tmp.png')) - cpos = result_z.plot_sectors(off_screen=True, screenshot=filename) + cpos = result_z.plot_sectors(screenshot=filename) assert isinstance(cpos, CameraPosition) assert os.path.isfile(filename) @skip_with_no_xserver def test_plot_sectors_x(result_x): - cpos = result_x.plot_sectors(off_screen=True) + cpos = result_x.plot_sectors() assert isinstance(cpos, CameraPosition) @@ -124,13 +124,13 @@ def test_plot_sectors_x(result_x): @skip_with_no_xserver @pytest.mark.skipif(result_z is None, reason="Requires result file") def test_plot_z_cyc(): - cpos = result_z.plot(off_screen=True) + cpos = result_z.plot() assert isinstance(cpos, CameraPosition) @skip_with_no_xserver def test_plot_x_cyc(result_x): - cpos = result_x.plot(off_screen=True) + cpos = result_x.plot() assert isinstance(cpos, CameraPosition) @@ -138,12 +138,11 @@ def test_plot_x_cyc(result_x): def test_plot_component_rotor(cyclic_v182_z_with_comp): cyclic_v182_z_with_comp.plot_nodal_solution(0, full_rotor=False, node_components='REFINE', - sel_type_all=False, - off_screen=True) + sel_type_all=False) cyclic_v182_z_with_comp.plot_nodal_solution(0, full_rotor=True, node_components='REFINE', - sel_type_all=False, off_screen=True) + sel_type_all=False) # result_z.plot_nodal_stress(20, 'Sx', node_components='REFINE', # sel_type_all=False, off_screen=True) @@ -276,7 +275,7 @@ def test_full_z_nodal_solution_phase(cyclic_v182_z): @skip_with_no_xserver def test_full_x_nodal_solution_plot(result_x): - result_x.plot_nodal_solution(0, off_screen=True) + result_x.plot_nodal_solution(0) def test_full_x_nodal_stress(result_x): @@ -349,7 +348,7 @@ def test_full_x_principal_nodal_stress(result_x): def test_animate_nodal_solution(tmpdir): temp_movie = str(tmpdir.mkdir("tmpdir").join('tmp.mp4')) result_z.animate_nodal_solution(0, nangles=20, movie_filename=temp_movie, - off_screen=True, loop=False) + loop=False) assert os.path.isfile(temp_movie) @@ -374,17 +373,17 @@ def test_cyclic_z_harmonic_displacement(): @skip_with_no_xserver def test_plot_nodal_stress(result_x): - result_x.plot_nodal_stress(0, 'z', off_screen=False) + result_x.plot_nodal_stress(0, 'z') @skip_with_no_xserver def test_plot_nodal_stress(result_x): - result_x.plot_nodal_stress(0, 'z', off_screen=True) + result_x.plot_nodal_stress(0, 'z') @skip_with_no_xserver def test_plot_principal_nodal_stress(result_x): - result_x.plot_principal_nodal_stress(0, 'seqv', off_screen=True) + result_x.plot_principal_nodal_stress(0, 'seqv') def test_nodal_elastic_strain_cyclic(result_x): @@ -405,7 +404,7 @@ def test_nodal_elastic_strain_cyclic(result_x): @skip_with_no_xserver def test_plot_nodal_elastic_strain(result_x): - result_x.plot_nodal_elastic_strain(0, 'X', off_screen=True) + result_x.plot_nodal_elastic_strain(0, 'X') def test_nodal_temperature(result_x): @@ -422,7 +421,7 @@ def test_nodal_temperature(result_x): @skip_with_no_xserver def test_plot_nodal_nodal_temperature(result_x): - result_x.plot_nodal_temperature(0, off_screen=True) + result_x.plot_nodal_temperature(0) def test_nodal_thermal_strain_cyclic(result_x): @@ -442,6 +441,4 @@ def test_nodal_thermal_strain_cyclic(result_x): @skip_with_no_xserver def test_plot_nodal_thermal_strain(result_x): - result_x.plot_nodal_thermal_strain(0, 'X', off_screen=True) - - + result_x.plot_nodal_thermal_strain(0, 'X')