From d810301147e4217654ddbfbbc381515c6267e6fa Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 13:53:28 -0600 Subject: [PATCH 01/20] Add files via upload Initial add for CfdOF remote processing --- CfdCaseWriterFoam.py | 746 +++++++++++++ CfdPreferencePage.py | 639 +++++++++++ CfdRemotePreferencePage.ui | 856 ++++++++++++++ CfdTools.py | 2043 ++++++++++++++++++++++++++++++++++ TaskPanelCfdMesh.py | 710 ++++++++++++ TaskPanelCfdMesh.ui | 611 ++++++++++ TaskPanelCfdSolverControl.ui | 278 +++++ 7 files changed, 5883 insertions(+) create mode 100644 CfdCaseWriterFoam.py create mode 100644 CfdPreferencePage.py create mode 100644 CfdRemotePreferencePage.ui create mode 100644 CfdTools.py create mode 100644 TaskPanelCfdMesh.py create mode 100644 TaskPanelCfdMesh.ui create mode 100644 TaskPanelCfdSolverControl.ui diff --git a/CfdCaseWriterFoam.py b/CfdCaseWriterFoam.py new file mode 100644 index 00000000..e049bfb7 --- /dev/null +++ b/CfdCaseWriterFoam.py @@ -0,0 +1,746 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 Alfred Bogaers (CSIR) * +# * Copyright (c) 2017 Johan Heyns (CSIR) * +# * Copyright (c) 2017 Oliver Oxtoby (CSIR) * +# * Copyright (c) 2019-2022 Oliver Oxtoby * +# * Copyright (c) 2022 Jonathan Bergh * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 3 of the * +# * License, or (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with this program. If not, * +# * see . * +# * * +# *************************************************************************** + +import os +import os.path +#import FreeCAD +from FreeCAD import Units, ParamGet +from CfdOF import CfdTools +from CfdOF.TemplateBuilder import TemplateBuilder +from CfdOF.CfdTools import cfdMessage +from CfdOF.Mesh import CfdMeshTools + + +class CfdCaseWriterFoam: + def __init__(self, analysis_obj): + self.case_folder = None + self.mesh_file_name = None + self.template_path = None + + self.analysis_obj = analysis_obj + self.solver_obj = CfdTools.getSolver(analysis_obj) + self.physics_model = CfdTools.getPhysicsModel(analysis_obj) + self.mesh_obj = CfdTools.getMesh(analysis_obj) + self.material_objs = CfdTools.getMaterials(analysis_obj) + self.bc_group = CfdTools.getCfdBoundaryGroup(analysis_obj) + self.initial_conditions = CfdTools.getInitialConditions(analysis_obj) + self.reporting_functions = CfdTools.getReportingFunctionsGroup(analysis_obj) + self.scalar_transport_objs = CfdTools.getScalarTransportFunctionsGroup(analysis_obj) + self.porous_zone_objs = CfdTools.getPorousZoneObjects(analysis_obj) + self.initialisation_zone_objs = CfdTools.getInitialisationZoneObjects(analysis_obj) + self.zone_objs = CfdTools.getZoneObjects(analysis_obj) + self.dynamic_mesh_refinement_obj = CfdTools.getDynamicMeshAdaptation(analysis_obj) + self.mesh_generated = False + self.working_dir = CfdTools.getOutputPath(self.analysis_obj) + self.progressCallback = None + + self.settings = None + + def writeCase(self, profile_name): + """ writeCase() will collect case settings, and finally build a runnable case. """ + cfdMessage("Writing case to folder {}\n".format(self.working_dir)) + if not os.path.exists(self.working_dir): + raise IOError("Path " + self.working_dir + " does not exist.") + + # Perform initialisation here rather than __init__ in case of path changes + self.case_folder = os.path.join(self.working_dir, self.solver_obj.InputCaseName) + self.case_folder = os.path.expanduser(os.path.abspath(self.case_folder)) + self.mesh_file_name = os.path.join(self.case_folder, self.solver_obj.InputCaseName, u".unv") + self.template_path = os.path.join(CfdTools.getModulePath(), "Data", "Templates", "case") + + # Collect settings into single dictionary + if not self.mesh_obj: + raise RuntimeError("No mesh object found in analysis") + phys_settings = CfdTools.propsToDict(self.physics_model) + + # Validate BC labels + bc_labels = [b.Label for b in self.bc_group] + for i, l in enumerate(bc_labels): + if bc_labels[i].find(' ') >= 0: + raise ValueError("Boundary condition label '" + bc_labels[i] + "' is not valid: May not contain spaces") + for i, l in enumerate(bc_labels): + for j in range(i+1, len(bc_labels)): + if bc_labels[j] == l: + raise ValueError("Boundary condition label '" + bc_labels[i] + "' is duplicated") + + self.settings = { + 'physics': phys_settings, + 'fluidProperties': [], # Order is important, so use a list + 'initialValues': CfdTools.propsToDict(self.initial_conditions), + 'boundaries': dict((b.Label, CfdTools.propsToDict(b)) for b in self.bc_group), + 'reportingFunctions': dict((fo.Label, CfdTools.propsToDict(fo)) for fo in self.reporting_functions), + 'reportingFunctionsEnabled': False, + 'scalarTransportFunctions': dict((st.Label, CfdTools.propsToDict(st)) for st in self.scalar_transport_objs), + 'scalarTransportFunctionsEnabled': False, + 'dynamicMesh': {}, + 'dynamicMeshEnabled': False, + 'bafflesPresent': self.bafflesPresent(), + 'porousZones': {}, + 'porousZonesPresent': False, + 'initialisationZones': {o.Label: CfdTools.propsToDict(o) for o in self.initialisation_zone_objs}, + 'initialisationZonesPresent': len(self.initialisation_zone_objs) > 0, + 'zones': {o.Label: {'PartNameList': tuple(r[0].Name for r in o.ShapeRefs)} for o in self.zone_objs}, + 'zonesPresent': len(self.zone_objs) > 0, + 'meshType': self.mesh_obj.Proxy.Type, + 'meshDimension': self.mesh_obj.ElementDimension, + 'meshDir': os.path.join(self.working_dir, self.mesh_obj.CaseName), + 'solver': CfdTools.propsToDict(self.solver_obj), + 'system': {}, + 'runChangeDictionary': False + } + + if CfdTools.DockerContainer.usedocker: + mesh_d = self.settings['meshDir'].split(os.sep) + self.settings['meshDir'] = '/tmp/{}'.format(mesh_d[-1]) + + self.processSystemSettings() + self.processSolverSettings() + self.processFluidProperties() + self.processBoundaryConditions() + self.processInitialConditions() + CfdTools.clearCase(self.case_folder) + + self.exportZoneStlSurfaces() + if self.porous_zone_objs: + self.processPorousZoneProperties() + self.processInitialisationZoneProperties() + + if self.reporting_functions: + cfdMessage('Reporting functions present') + self.processReportingFunctions() + + if self.scalar_transport_objs: + cfdMessage('Scalar transport functions present') + self.processScalarTransportFunctions() + + if self.dynamic_mesh_refinement_obj: + cfdMessage('Dynamic mesh adaptation rule present') + self.processDynamicMeshRefinement() + + self.settings['createPatchesFromSnappyBaffles'] = False + cfdMessage("Matching boundary conditions ...\n") + if self.progressCallback: + self.progressCallback("Matching boundary conditions ...") + self.setupPatchNames() + + TemplateBuilder(self.case_folder, self.template_path, self.settings) + + # Update Allrun permission - will fail silently on Windows + file_name = os.path.join(self.case_folder, "Allrun") + import stat + s = os.stat(file_name) + os.chmod(file_name, s.st_mode | stat.S_IEXEC) + + cfdMessage("Successfully wrote case to folder {}\n".format(self.working_dir)) + if self.progressCallback: + self.progressCallback("Case written locally successfully") + + # if using a remote host, copy the case folder from the local case dir + # to the remote host's directory + if profile_name != "local": + profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + profile_name + remote_user = ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = ParamGet(profile_prefs).GetString("OutputPath","") + + #print("remote_user:" + remote_user) + #print("remote_hostname:" + remote_hostname) + #print("remote_output_path:" + remote_output_path) + #print("self.case_folder:" + self.case_folder) + #print("self.working_dir:" + self.working_dir) + + # rsync the meshCase directory to the remote host's output directory + # Typical useage: rsync -r --delete /tmp/ me@david:/tmp + try: + CfdTools.runFoamCommand("rsync -r --delete " + self.case_folder + " " + remote_user + "@" + remote_hostname + \ + ":" + remote_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy case to remote host: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not copy case to remote host: " + str(e)) + return False + else: + CfdTools.cfdMessage("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n" ) + if self.progressCallback: + self.progressCallback("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n") + + return True + + def getSolverName(self): + """ + Solver name is selected based on selected physics. This should only be extended as additional physics are + included. + """ + solver = None + if self.physics_model.Phase == 'Single': + if len(self.material_objs) == 1: + if self.physics_model.Flow == 'Incompressible': + if self.physics_model.Thermal == 'None': + if self.physics_model.Time == 'Transient': + solver = 'pimpleFoam' + else: + if self.porous_zone_objs or self.porousBafflesPresent(): + solver = 'porousSimpleFoam' + else: + solver = 'simpleFoam' + else: + raise RuntimeError("Only isothermal simulation currently supported for incompressible flow.") + elif self.physics_model.Flow == 'Compressible': + if self.physics_model.Time == 'Transient': + solver = 'buoyantPimpleFoam' + else: + solver = 'buoyantSimpleFoam' + elif self.physics_model.Flow == 'HighMachCompressible': + solver = 'hisa' + else: + raise RuntimeError(self.physics_model.Flow + " flow model currently not supported.") + else: + raise RuntimeError("Only one material object may be present for single phase simulation.") + elif self.physics_model.Phase == 'FreeSurface': + if self.physics_model.Time == 'Transient': + if self.physics_model.Thermal == 'None': + if len(self.material_objs) == 2: + solver = 'interFoam' + elif len(self.material_objs) > 2: + solver = 'multiphaseInterFoam' + else: + raise RuntimeError("At least two material objects must be present for free surface simulation.") + else: + raise RuntimeError("Only isothermal analysis is currently supported for free surface flow simulation.") + else: + raise RuntimeError("Only transient analysis is supported for free surface flow simulation.") + else: + raise RuntimeError(self.physics_model.Phase + " phase model currently not supported.") + + # Catch-all in case + if solver is None: + raise RuntimeError("No solver is supported to handle the selected physics with {} phases.".format( + len(self.material_objs))) + return solver + + def processSolverSettings(self): + solver_settings = self.settings['solver'] + if solver_settings['Parallel']: + if solver_settings['ParallelCores'] < 2: + solver_settings['ParallelCores'] = 2 + solver_settings['SolverName'] = self.getSolverName() + + def processSystemSettings(self): + system_settings = self.settings['system'] + system_settings['FoamRuntime'] = CfdTools.getFoamRuntime() + system_settings['CasePath'] = self.case_folder + system_settings['FoamPath'] = CfdTools.getFoamDir() + if CfdTools.getFoamRuntime() != 'WindowsDocker': + system_settings['TranslatedFoamPath'] = CfdTools.translatePath(CfdTools.getFoamDir()) + + def setupMesh(self, updated_mesh_path, scale): + if os.path.exists(updated_mesh_path): + CfdTools.convertMesh(self.case_folder, updated_mesh_path, scale) + + def processFluidProperties(self): + # self.material_obj currently stores everything as a string + # Convert to (mostly) SI numbers for OpenFOAM + settings = self.settings + for material_obj in self.material_objs: + mp = material_obj.Material + mp['Name'] = material_obj.Label + if 'Density' in mp: + mp['Density'] = Units.Quantity(mp['Density']).getValueAs("kg/m^3").Value + if 'DynamicViscosity' in mp: + if self.physics_model.Turbulence == 'Inviscid': + mp['DynamicViscosity'] = 0.0 + else: + mp['DynamicViscosity'] = Units.Quantity(mp['DynamicViscosity']).getValueAs("kg/m/s").Value + mp['KinematicViscosity'] = mp['DynamicViscosity']/mp['Density'] + if 'MolarMass' in mp: + # OpenFOAM uses kg/kmol + mp['MolarMass'] = Units.Quantity(mp['MolarMass']).getValueAs("kg/mol").Value*1000 + if 'Cp' in mp: + mp['Cp'] = Units.Quantity(mp['Cp']).getValueAs("J/kg/K").Value + if 'SutherlandTemperature' in mp: + mp['SutherlandTemperature'] = Units.Quantity(mp['SutherlandTemperature']).getValueAs("K").Value + if 'SutherlandRefViscosity' in mp and 'SutherlandRefTemperature' in mp: + mu0 = Units.Quantity(mp['SutherlandRefViscosity']).getValueAs("kg/m/s").Value + T0 = Units.Quantity(mp['SutherlandRefTemperature']).getValueAs("K").Value + mp['SutherlandConstant'] = mu0/T0**(3./2)*(T0+mp['SutherlandTemperature']) + for k in mp: + if k.endswith('Polynomial'): + poly = mp[k].split() + poly8 = [0.0]*8 + for i in range(len(poly)): + try: + poly8[i] = float(poly[i]) + except ValueError: + raise ValueError("Invalid coefficient {} in polynomial coefficient {}".format(poly[i], k)) + mp[k] = ' '.join(str(v) for v in poly8) + + settings['fluidProperties'].append(mp) + + def processBoundaryConditions(self): + """ Compute any quantities required before case build """ + settings = self.settings + # Copy keys so that we can delete while iterating + bc_names = list(settings['boundaries'].keys()) + for bc_name in bc_names: + bc = settings['boundaries'][bc_name] + if not bc['VelocityIsCartesian']: + veloMag = bc['VelocityMag'] + face = bc['DirectionFace'].split(':') + if not face[0]: + face = bc['ShapeRefs'][0].Name + # See if entered face actually exists and is planar + try: + selected_object = self.analysis_obj.Document.getObject(face[0]) + if hasattr(selected_object, "Shape"): + elt = selected_object.Shape.getElement(face[1]) + if elt.ShapeType == 'Face' and CfdTools.isPlanar(elt): + n = elt.normalAt(0.5, 0.5) + if bc['ReverseNormal']: + n = [-ni for ni in n] + velocity = [ni*veloMag for ni in n] + bc['Ux'] = velocity[0] + bc['Uy'] = velocity[1] + bc['Uz'] = velocity[2] + else: + raise RuntimeError + else: + raise RuntimeError + except (SystemError, RuntimeError): + raise RuntimeError(str(bc['DirectionFace']) + " is not a valid, planar face.") + if settings['solver']['SolverName'] in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam']: + bc['KinematicPressure'] = bc['Pressure']/settings['fluidProperties'][0]['Density'] + + if bc['PorousBaffleMethod'] == 'porousScreen': + wireDiam = bc['ScreenWireDiameter'] + spacing = bc['ScreenSpacing'] + CD = 1.0 # Drag coeff of wire (Simmons - valid for Re > ~300) + beta = (1-wireDiam/spacing)**2 + bc['PressureDropCoeff'] = CD*(1-beta) + + if settings['solver']['SolverName'] in ['interFoam', 'multiphaseInterFoam']: + # Make sure the first n-1 alpha values exist, and write the n-th one + # consistently for multiphaseInterFoam + sum_alpha = 0.0 + alphas_new = {} + for i, m in enumerate(settings['fluidProperties']): + alpha_name = m['Name'] + if i == len(settings['fluidProperties']) - 1: + if settings['solver']['SolverName'] == 'multiphaseInterFoam': + alphas_new[alpha_name] = 1.0 - sum_alpha + else: + alpha = Units.Quantity(bc.get('VolumeFractions', {}).get(alpha_name, '0')).Value + alphas_new[alpha_name] = alpha + sum_alpha += alpha + bc['VolumeFractions'] = alphas_new + + # Copy turbulence settings + bc['TurbulenceIntensity'] = bc['TurbulenceIntensityPercentage']/100.0 + physics = settings['physics'] + if physics['Turbulence'] == 'RANS' and physics['TurbulenceModel'] == 'SpalartAllmaras': + if (bc['BoundaryType'] == 'inlet' or bc['BoundaryType'] == 'open') and \ + bc['TurbulenceInletSpecification'] == 'intensityAndLengthScale': + if bc['BoundarySubType'] == 'uniformVelocityInlet' or bc['BoundarySubType'] == 'farField': + Uin = (bc['Ux']**2 + bc['Uy']**2 + bc['Uz']**2)**0.5 + + # Turb Intensity and length scale + I = bc['TurbulenceIntensity'] + l = bc['TurbulenceLengthScale'] + + # Spalart Allmaras + bc['NuTilda'] = (3.0/2.0)**0.5 * Uin * I * l + + else: + raise RuntimeError( + "Inlet type currently unsupported for calculating turbulence inlet conditions from " + "intensity and length scale.") + + if bc['DefaultBoundary']: + if settings['boundaries'].get('defaultFaces'): + raise ValueError("More than one default boundary defined") + settings['boundaries']['defaultFaces'] = bc + if not settings['boundaries'].get('defaultFaces'): + settings['boundaries']['defaultFaces'] = { + 'BoundaryType': 'wall', + 'BoundarySubType': 'slipWall', + 'ThermalBoundaryType': 'zeroGradient' + } + + # Assign any extruded patches as the appropriate type + mr_objs = CfdTools.getMeshRefinementObjs(self.mesh_obj) + for mr_id, mr_obj in enumerate(mr_objs): + if mr_obj.Extrusion and mr_obj.ExtrusionType == "2DPlanar": + settings['boundaries'][mr_obj.Label] = { + 'BoundaryType': 'constraint', + 'BoundarySubType': 'empty' + } + settings['boundaries'][mr_obj.Label+"BackFace"] = { + 'BoundaryType': 'constraint', + 'BoundarySubType': 'empty' + } + if mr_obj.Extrusion and mr_obj.ExtrusionType == "2DWedge": + settings['boundaries'][mr_obj.Label] = { + 'BoundaryType': 'constraint', + 'BoundarySubType': 'symmetry' + } + settings['boundaries'][mr_obj.Label+"BackFace"] = { + 'BoundaryType': 'constraint', + 'BoundarySubType': 'symmetry' + } + + def parseFaces(self, shape_refs): + pass + + def processInitialConditions(self): + """ Do any required computations before case build. Boundary conditions must be processed first. """ + settings = self.settings + initial_values = settings['initialValues'] + if settings['solver']['SolverName'] in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam']: + mat_prop = settings['fluidProperties'][0] + initial_values['KinematicPressure'] = initial_values['Pressure'] / mat_prop['Density'] + if settings['solver']['SolverName'] in ['interFoam', 'multiphaseInterFoam']: + # Make sure the first n-1 alpha values exist, and write the n-th one + # consistently for multiphaseInterFoam + sum_alpha = 0.0 + alphas_new = {} + for i, m in enumerate(settings['fluidProperties']): + alpha_name = m['Name'] + if i == len(settings['fluidProperties'])-1: + if settings['solver']['SolverName'] == 'multiphaseInterFoam': + alphas_new[alpha_name] = 1.0-sum_alpha + else: + alpha = Units.Quantity(initial_values.get('VolumeFractions', {}).get(alpha_name, '0')).Value + alphas_new[alpha_name] = alpha + sum_alpha += alpha + initial_values['VolumeFractions'] = alphas_new + + if initial_values['PotentialFlowP']: + if settings['solver']['SolverName'] not in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam', 'hisa']: + raise RuntimeError("Selected solver does not support potential pressure initialisation.") + + physics = settings['physics'] + + # Copy velocity + if initial_values['UseInletUValues']: + if initial_values['BoundaryU']: + inlet_bc = settings['boundaries'][initial_values['BoundaryU'].Label] + if inlet_bc['BoundarySubType'] == 'uniformVelocityInlet' or inlet_bc['BoundarySubType'] == 'farField': + initial_values['Ux'] = inlet_bc['Ux'] + initial_values['Uy'] = inlet_bc['Uy'] + initial_values['Uz'] = inlet_bc['Uz'] + else: + raise RuntimeError("Boundary type not appropriate to determine initial velocity.") + else: + raise RuntimeError("No boundary selected to copy initial velocity value from.") + + # Copy pressure + if initial_values['UseOutletPValue']: + if initial_values['BoundaryP']: + outlet_bc = settings['boundaries'][initial_values['BoundaryP'].Label] + if outlet_bc['BoundarySubType'] == 'staticPressureOutlet' or \ + outlet_bc['BoundarySubType'] == 'totalPressureOpening' or \ + outlet_bc['BoundarySubType'] == 'totalPressureInlet' or \ + outlet_bc['BoundarySubType'] == 'staticPressureInlet' or \ + outlet_bc['BoundarySubType'] == 'farField': + initial_values['Pressure'] = outlet_bc['Pressure'] + else: + raise RuntimeError("Boundary type not appropriate to determine initial pressure.") + else: + raise RuntimeError("No boundary selected to copy initial pressure value from.") + + if physics['Thermal'] == 'Energy' and initial_values['UseInletTemperatureValue']: + inlet_bc = settings['boundaries'][initial_values['BoundaryT'].Label] + if inlet_bc['BoundaryType'] == 'inlet': + if inlet_bc['ThermalBoundaryType'] == 'fixedValue': + initial_values['Temperature'] = inlet_bc['Temperature'] + else: + raise RuntimeError("Inlet type not appropriate to determine initial temperature") + elif inlet_bc['BoundarySubType'] == 'farField': + initial_values['Temperature'] = inlet_bc['Temperature'] + else: + raise RuntimeError("Inlet type not appropriate to determine initial temperature.") + + # Copy turbulence settings + if physics['TurbulenceModel'] is not None: + if initial_values['UseInletTurbulenceValues']: + if initial_values['BoundaryTurb']: + inlet_bc = settings['boundaries'][initial_values['BoundaryTurb'].Label] + + if inlet_bc['TurbulenceInletSpecification'] == 'TKEAndSpecDissipationRate': + initial_values['k'] = inlet_bc['TurbulentKineticEnergy'] + initial_values['omega'] = inlet_bc['SpecificDissipationRate'] + elif inlet_bc['TurbulenceInletSpecification'] == 'TKEAndDissipationRate': + initial_values['k'] = inlet_bc['TurbulentKineticEnergy'] + initial_values['epsilon'] = inlet_bc['DissipationRate'] + elif inlet_bc['TurbulenceInletSpecification'] == 'TransportedNuTilda': + initial_values['nuTilda'] = inlet_bc['NuTilda'] + elif inlet_bc['TurbulenceInletSpecification'] == 'TKESpecDissipationRateGammaAndReThetat': + initial_values['k'] = inlet_bc['TurbulentKineticEnergy'] + initial_values['omega'] = inlet_bc['SpecificDissipationRate'] + initial_values['gammaInt'] = inlet_bc['Intermittency'] + initial_values['ReThetat'] = inlet_bc['ReThetat'] + elif inlet_bc['TurbulenceInletSpecification'] == 'TurbulentViscosity': + initial_values['nut'] = inlet_bc['TurbulentViscosity'] + elif inlet_bc['TurbulenceInletSpecification'] == 'TurbulentViscosityAndK': + initial_values['kEqnk'] = inlet_bc['kEqnTurbulentKineticEnergy'] + initial_values['kEqnNut'] = inlet_bc['kEqnTurbulentViscosity'] + elif inlet_bc['TurbulenceInletSpecification'] == 'intensityAndLengthScale': + if inlet_bc['BoundarySubType'] == 'uniformVelocityInlet' or \ + inlet_bc['BoundarySubType'] == 'farField': + Uin = (inlet_bc['Ux']**2 + + inlet_bc['Uy']**2 + + inlet_bc['Uz']**2)**0.5 + + # Turb Intensity (or Tu) and length scale + I = inlet_bc['TurbulenceIntensityPercentage'] / 100.0 # Convert from percent to fraction + l = inlet_bc['TurbulenceLengthScale'] + Cmu = 0.09 # Standard turbulence model parameter + + # k omega, k epsilon + k = 3.0/2.0*(Uin*I)**2 + omega = k**0.5/(Cmu**0.25*l) + epsilon = (k**(3.0/2.0) * Cmu**0.75) / l + + # Spalart Allmaras + nuTilda = inlet_bc['NuTilda'] + + # k omega (transition) + gammaInt = 1 + if I <= 1.3: + ReThetat = 1173.51 - (589.428 * I) + (0.2196 / (I**2)) + else: + ReThetat = 331.5 / ((I - 0.5658)**0.671) + + # Set the values + initial_values['k'] = k + initial_values['omega'] = omega + initial_values['epsilon'] = epsilon + initial_values['nuTilda'] = nuTilda + initial_values['gammaInt'] = gammaInt + initial_values['ReThetat'] = ReThetat + + else: + raise RuntimeError( + "Inlet type currently unsupported for copying turbulence initial conditions.") + else: + raise RuntimeError( + "Turbulence inlet specification currently unsupported for copying turbulence initial conditions") + else: + raise RuntimeError("No boundary selected to copy initial turbulence values from.") + #TODO: Check that the required values have actually been set for each turbulent model + + # Function objects (reporting functions, probes) + def processReportingFunctions(self): + """ Compute any Function objects required before case build """ + settings = self.settings + settings['reportingFunctionsEnabled'] = True + + for name in settings['reportingFunctions']: + rf = settings['reportingFunctions'][name] + + rf['PatchName'] = rf['Patch'].Label + rf['CentreOfRotation'] = \ + tuple(Units.Quantity(p, Units.Length).getValueAs('m') for p in rf['CentreOfRotation']) + rf['Direction'] = tuple(p for p in rf['Direction']) + + if rf['ReportingFunctionType'] == 'ForceCoefficients': + pitch_axis = rf['Lift'].cross(rf['Drag']) + rf['Lift'] = tuple(p for p in rf['Lift']) + rf['Drag'] = tuple(p for p in rf['Drag']) + rf['Pitch'] = tuple(p for p in pitch_axis) + + settings['reportingFunctions'][name]['ProbePosition'] = tuple( + Units.Quantity(p, Units.Length).getValueAs('m') + for p in settings['reportingFunctions'][name]['ProbePosition']) + + def processScalarTransportFunctions(self): + settings = self.settings + settings['scalarTransportFunctionsEnabled'] = True + for name in settings['scalarTransportFunctions']: + stf = settings['scalarTransportFunctions'][name] + if settings['solver']['SolverName'] in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam']: + stf['InjectionRate'] = stf['InjectionRate']/settings['fluidProperties'][0]['Density'] + stf['DiffusivityFixedValue'] = stf['DiffusivityFixedValue']/settings['fluidProperties'][0]['Density'] + stf['InjectionPoint'] = tuple( + Units.Quantity(p, Units.Length).getValueAs('m') for p in stf['InjectionPoint']) + + # Mesh related + def processDynamicMeshRefinement(self): + settings = self.settings + settings['dynamicMeshEnabled'] = True + + # Check whether transient + if not self.physics_model.Time == 'Transient': + raise RuntimeError("Dynamic mesh refinement is not supported by steady-state solvers") + + # Check whether cellLevel supported + if self.mesh_obj.MeshUtility not in ['cfMesh', 'snappyHexMesh']: + raise RuntimeError("Dynamic mesh refinement is only supported by cfMesh and snappyHexMesh") + + # Check whether 2D extrusion present + mesh_refinements = CfdTools.getMeshRefinementObjs(self.mesh_obj) + for mr in mesh_refinements: + if mr.Extrusion: + if mr.ExtrusionType == '2DPlanar' or mr.ExtrusionType == '2DWedge': + raise RuntimeError("Dynamic mesh refinement will not work with 2D or wedge mesh") + + settings['dynamicMesh'] = CfdTools.propsToDict(self.dynamic_mesh_refinement_obj) + + # Zones + def exportZoneStlSurfaces(self): + for zo in self.zone_objs: + for r in zo.ShapeRefs: + path = os.path.join(self.working_dir, + self.solver_obj.InputCaseName, + "constant", + "triSurface") + if not os.path.exists(path): + os.makedirs(path) + sel_obj = r[0] + shape = sel_obj.Shape + CfdMeshTools.writeSurfaceMeshFromShape(shape, path, r[0].Name, self.mesh_obj) + print("Successfully wrote stl surface\n") + + def processPorousZoneProperties(self): + settings = self.settings + settings['porousZonesPresent'] = True + porousZoneSettings = settings['porousZones'] + for po in self.porous_zone_objs: + pd = {'PartNameList': tuple(r[0].Name for r in po.ShapeRefs)} + po = CfdTools.propsToDict(po) + if po['PorousCorrelation'] == 'DarcyForchheimer': + pd['D'] = (po['D1'], po['D2'], po['D3']) + pd['F'] = (po['F1'], po['F2'], po['F3']) + pd['e1'] = tuple(po['e1']) + pd['e3'] = tuple(po['e3']) + elif po['PorousCorrelation'] == 'Jakob': + # Calculate effective Darcy-Forchheimer coefficients + # This is for equilateral triangles arranged with the triangles pointing in BundleLayerNormal + # direction (direction of greater spacing - sqrt(3)*triangleEdgeLength) + pd['e1'] = tuple(po['SpacingDirection']) # OpenFOAM modifies to be orthog to e3 + pd['e3'] = tuple(po['TubeAxis']) + spacing = po['TubeSpacing'] + d0 = po['OuterDiameter'] + u0 = po['VelocityEstimate'] + aspectRatio = po['AspectRatio'] + kinVisc = self.settings['fluidProperties']['KinematicViscosity'] + if kinVisc == 0.0: + raise ValueError("Viscosity must be set for Jakob correlation") + if spacing < d0: + raise ValueError("Tube spacing may not be less than diameter") + D = [0, 0, 0] + F = [0, 0, 0] + for (i, Sl, St) in [(0, aspectRatio*spacing, spacing), (1, spacing, aspectRatio*spacing)]: + C = 1.0/St*0.5*(1.0+0.47/(Sl/d0-1)**1.06)*(1.0/(1-d0/Sl))**(2.0-0.16) + Di = C/d0*0.5*(u0*d0/kinVisc)**(1.0-0.16) + Fi = C*(u0*d0/kinVisc)**(-0.16) + D[i] = Di + F[i] = Fi + pd['D'] = tuple(D) + pd['F'] = tuple(F) + # Currently assuming zero drag parallel to tube bundle (3rd principal dirn) + else: + raise RuntimeError("Unrecognised method for porous baffle resistance") + porousZoneSettings[po['Label']] = pd + + def processInitialisationZoneProperties(self): + settings = self.settings + if settings['solver']['SolverName'] in ['interFoam', 'multiphaseInterFoam']: + # Make sure the first n-1 alpha values exist, and write the n-th one + # consistently for multiphaseInterFoam + for zone_name in settings['initialisationZones']: + z = settings['initialisationZones'][zone_name] + sum_alpha = 0.0 + if 'VolumeFractions' in z: + alphas_new = {} + for i, m in enumerate(settings['fluidProperties']): + alpha_name = m['Name'] + if i == len(settings['fluidProperties'])-1: + if settings['solver']['SolverName'] == 'multiphaseInterFoam': + alphas_new[alpha_name] = 1.0-sum_alpha + else: + alpha = Units.Quantity(z['VolumeFractions'].get(alpha_name, '0')).Value + alphas_new[alpha_name] = alpha + sum_alpha += alpha + z['VolumeFractions'] = alphas_new + + def bafflesPresent(self): + for b in self.bc_group: + if b.BoundaryType == 'baffle': + return True + return False + + def porousBafflesPresent(self): + for b in self.bc_group: + if b.BoundaryType == 'baffle' and \ + b.BoundarySubType == 'porousBaffle': + return True + return False + + def setupPatchNames(self): + print('Populating createPatchDict to update BC names') + settings = self.settings + settings['createPatches'] = {} + settings['createPatchesSnappyBaffles'] = {} + bc_group = self.bc_group + + defaultPatchType = "patch" + for bc_id, bc_obj in enumerate(bc_group): + bcType = bc_obj.BoundaryType + bcSubType = bc_obj.BoundarySubType + patchType = CfdTools.getPatchType(bcType, bcSubType) + settings['createPatches'][bc_obj.Label] = { + 'PatchNamesList': '"patch_'+str(bc_id+1)+'_.*"', + 'PatchType': patchType + } + if bc_obj.DefaultBoundary: + defaultPatchType = patchType + + if bcType == 'baffle' and self.mesh_obj.MeshUtility == 'snappyHexMesh': + settings['createPatchesFromSnappyBaffles'] = True + settings['createPatchesSnappyBaffles'][bc_obj.Label] = { + 'PatchNamesList': '"'+bc_obj.Name+'_[^_]*"', + 'PatchNamesListSlave': '"'+bc_obj.Name+'_.*_slave"'} + + # Set up default BC for unassigned faces + settings['createPatches']['defaultFaces'] = { + 'PatchNamesList': '"patch_0_0"', + 'PatchType': defaultPatchType + } + + # Assign any extruded patches as the appropriate type + mr_objs = CfdTools.getMeshRefinementObjs(self.mesh_obj) + for mr_id, mr_obj in enumerate(mr_objs): + if mr_obj.Extrusion and mr_obj.ExtrusionType == "2DPlanar": + settings['createPatches'][mr_obj.Label] = { + 'PatchNamesList': '"patch_.*_'+str(mr_id+1)+'"', + 'PatchType': "empty" + } + elif mr_obj.Extrusion and mr_obj.ExtrusionType == "2DWedge": + settings['createPatches'][mr_obj.Label] = { + 'PatchNamesList': '"patch_.*_'+str(mr_id+1)+'"', + 'PatchType': "symmetry" + } + else: + # Add others to default faces list + settings['createPatches']['defaultFaces']['PatchNamesList'] += ' "patch_0_'+str(mr_id+1) + '"' diff --git a/CfdPreferencePage.py b/CfdPreferencePage.py new file mode 100644 index 00000000..d078672d --- /dev/null +++ b/CfdPreferencePage.py @@ -0,0 +1,639 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017-2018 Oliver Oxtoby (CSIR) * +# * Copyright (c) 2017 Johan Heyns (CSIR) * +# * Copyright (c) 2017 Alfred Bogaers (CSIR) * +# * Copyright (c) 2019-2022 Oliver Oxtoby * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 3 of the * +# * License, or (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with this program. If not, * +# * see . * +# * * +# *************************************************************************** + +import os +import os.path +import platform +import sys +if sys.version_info >= (3,): # Python 3 + import urllib.request as urlrequest + import urllib.parse as urlparse +else: + import urllib as urlrequest + import urlparse + +import ssl +import ctypes +import FreeCAD +from CfdOF import CfdTools +import tempfile +from contextlib import closing +from xml.sax.saxutils import escape + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore + from PySide import QtGui + from PySide.QtCore import Qt, QObject, QThread + from PySide.QtGui import QApplication + + +# Constants +OPENFOAM_URL = \ + "https://sourceforge.net/projects/openfoam/files/v2206/OpenFOAM-v2206-windows-mingw.exe/download" +OPENFOAM_FILE_EXT = ".exe" +PARAVIEW_URL = \ + "https://www.paraview.org/paraview-downloads/download.php?submit=Download&version=v5.10&type=binary&os=Windows&downloadFile=ParaView-5.10.1-Windows-Python3.9-msvc2017-AMD64.exe" +PARAVIEW_FILE_EXT = ".exe" +CFMESH_URL = \ + "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof.zip/download" +CFMESH_URL_MINGW = \ + "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof-binaries-{}.zip/download" +CFMESH_FILE_BASE = "cfmesh-cfdof" +CFMESH_FILE_EXT = ".zip" +HISA_URL = \ + "https://sourceforge.net/projects/hisa/files/hisa-master.zip/download" +HISA_URL_MINGW = \ + "https://sourceforge.net/projects/hisa/files/hisa-master-binaries-{}.zip/download" +HISA_FILE_BASE = "hisa-master" +HISA_FILE_EXT = ".zip" +DOCKER_URL = \ + "docker.io/mmcker/cfdof-openfoam" + +# Tasks for the worker thread +DOWNLOAD_OPENFOAM = 1 +DOWNLOAD_PARAVIEW = 2 +DOWNLOAD_CFMESH = 3 +DOWNLOAD_HISA = 4 +DOWNLOAD_DOCKER = 5 + +class CloseDetector(QObject): + def __init__(self, obj, callback): + super().__init__(obj) + self.callback = callback + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.ChildRemoved: + self.callback() + return False + + +class CfdPreferencePage: + def __init__(self): + ui_path = os.path.join(CfdTools.getModulePath(), 'Gui', "CfdPreferencePage.ui") + self.form = FreeCADGui.PySideUic.loadUi(ui_path) + + self.form.tb_choose_foam_dir.clicked.connect(self.chooseFoamDir) + self.form.le_foam_dir.textChanged.connect(self.foamDirChanged) + self.form.tb_choose_paraview_path.clicked.connect(self.chooseParaviewPath) + self.form.le_paraview_path.textChanged.connect(self.paraviewPathChanged) + self.form.tb_choose_gmsh_path.clicked.connect(self.chooseGmshPath) + self.form.le_gmsh_path.textChanged.connect(self.gmshPathChanged) + self.form.pb_run_dependency_checker.clicked.connect(self.runDependencyChecker) + self.form.pb_download_install_openfoam.clicked.connect(self.downloadInstallOpenFoam) + self.form.tb_pick_openfoam_file.clicked.connect(self.pickOpenFoamFile) + self.form.pb_download_install_paraview.clicked.connect(self.downloadInstallParaview) + self.form.tb_pick_paraview_file.clicked.connect(self.pickParaviewFile) + self.form.pb_download_install_cfMesh.clicked.connect(self.downloadInstallCfMesh) + self.form.tb_pick_cfmesh_file.clicked.connect(self.pickCfMeshFile) + self.form.pb_download_install_hisa.clicked.connect(self.downloadInstallHisa) + self.form.tb_pick_hisa_file.clicked.connect(self.pickHisaFile) + + self.form.le_openfoam_url.setText(OPENFOAM_URL) + self.form.le_paraview_url.setText(PARAVIEW_URL) + + self.form.tb_choose_output_dir.clicked.connect(self.chooseOutputDir) + self.form.le_output_dir.textChanged.connect(self.outputDirChanged) + + self.form.cb_docker_sel.clicked.connect(self.dockerCheckboxClicked) + self.form.pb_download_install_docker.clicked.connect(self.downloadInstallDocker) + + self.dockerCheckboxClicked() + + self.ev_filter = CloseDetector(self.form, self.cleanUp) + self.form.installEventFilter(self.ev_filter) + + self.thread = None + self.install_process = None + + self.console_message = "" + + self.foam_dir = "" + self.initial_foam_dir = "" + + self.paraview_path = "" + self.initial_paraview_path = "" + + self.gmsh_path = "" + self.initial_gmsh_path = "" + + self.output_dir = "" + + self.form.gb_openfoam.setVisible(platform.system() == 'Windows') + self.form.gb_paraview.setVisible(platform.system() == 'Windows') + + def __del__(self): + self.cleanUp() + + def cleanUp(self): + if self.thread and self.thread.isRunning(): + FreeCAD.Console.PrintError("Terminating a pending install task\n") + self.thread.quit = True + if self.install_process and self.install_process.state() == QtCore.QProcess.Running: + FreeCAD.Console.PrintError("Terminating a pending install task\n") + self.install_process.terminate() + QApplication.restoreOverrideCursor() + + def saveSettings(self): + CfdTools.setFoamDir(self.foam_dir) + CfdTools.setParaviewPath(self.paraview_path) + CfdTools.setGmshPath(self.gmsh_path) + prefs = CfdTools.getPreferencesLocation() + FreeCAD.ParamGet(prefs).SetString("DefaultOutputPath", self.output_dir) + FreeCAD.ParamGet(prefs).SetBool("UseDocker",self.form.cb_docker_sel.isChecked()) + FreeCAD.ParamGet(prefs).SetString("DockerURL",self.form.le_docker_url.text()) + + def loadSettings(self): + # Don't set the autodetected location, since the user might want to allow that to vary according + # to WM_PROJECT_DIR setting + prefs = CfdTools.getPreferencesLocation() + self.foam_dir = FreeCAD.ParamGet(prefs).GetString("InstallationPath", "") + self.initial_foam_dir = str(self.foam_dir) + self.form.le_foam_dir.setText(self.foam_dir) + + self.paraview_path = CfdTools.getParaviewPath() + self.initial_paraview_path = str(self.paraview_path) + self.form.le_paraview_path.setText(self.paraview_path) + + self.gmsh_path = CfdTools.getGmshPath() + self.initial_gmsh_path = str(self.gmsh_path) + self.form.le_gmsh_path.setText(self.gmsh_path) + + self.output_dir = CfdTools.getDefaultOutputPath() + self.form.le_output_dir.setText(self.output_dir) + + if FreeCAD.ParamGet(prefs).GetBool("UseDocker", 0): + self.form.cb_docker_sel.setCheckState(Qt.Checked) + + # Set usedocker and enable/disable download buttons + self.dockerCheckboxClicked() + + self.form.le_docker_url.setText(FreeCAD.ParamGet(prefs).GetString("DockerURL", DOCKER_URL)) + + self.setDownloadURLs() + + def consoleMessage(self, message="", colour_type=None): + message = escape(message) + message = message.replace('\n', '
') + if colour_type: + self.console_message += '{1}
'.format(CfdTools.getColour(colour_type), message) + else: + self.console_message += message+'
' + self.form.textEdit_Output.setText(self.console_message) + self.form.textEdit_Output.moveCursor(QtGui.QTextCursor.End) + + def foamDirChanged(self, text): + self.foam_dir = text + if self.foam_dir and os.access(self.foam_dir, os.R_OK): + self.setDownloadURLs() + + def testGetRuntime(self, disable_exception=True): + """ Set the foam dir temporarily and see if we can detect the runtime """ + CfdTools.setFoamDir(self.foam_dir) + try: + runtime = CfdTools.getFoamRuntime() + except IOError as e: + runtime = None + if not disable_exception: + raise + CfdTools.setFoamDir(self.initial_foam_dir) + return runtime + + def setDownloadURLs(self): + if self.testGetRuntime() == "MinGW": + # Temporarily apply the foam dir selection + CfdTools.setFoamDir(self.foam_dir) + foam_ver = os.path.split(CfdTools.getFoamDir())[-1] + self.form.le_cfmesh_url.setText(CFMESH_URL_MINGW.format(foam_ver)) + self.form.le_hisa_url.setText(HISA_URL_MINGW.format(foam_ver)) + CfdTools.setFoamDir(self.initial_foam_dir) + else: + self.form.le_cfmesh_url.setText(CFMESH_URL) + self.form.le_hisa_url.setText(HISA_URL) + + def paraviewPathChanged(self, text): + self.paraview_path = text + + def gmshPathChanged(self, text): + self.gmsh_path = text + + def chooseFoamDir(self): + d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose OpenFOAM directory', self.foam_dir) + if d and os.access(d, os.R_OK): + self.foam_dir = d + self.form.le_foam_dir.setText(self.foam_dir) + + def chooseParaviewPath(self): + p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView executable', self.paraview_path, + filter="*.exe" if platform.system() == 'Windows' else None) + if p and os.access(p, os.R_OK): + self.paraview_path = p + self.form.le_paraview_path.setText(self.paraview_path) + + def chooseGmshPath(self): + p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose gmsh executable', self.gmsh_path, + filter="*.exe" if platform.system() == 'Windows' else None) + if p and os.access(p, os.R_OK): + self.gmsh_path = p + self.form.le_gmsh_path.setText(self.gmsh_path) + + def outputDirChanged(self, text): + self.output_dir = text + + def chooseOutputDir(self): + d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose output directory', self.output_dir) + if d and os.access(d, os.W_OK): + self.output_dir = os.path.abspath(d) + self.form.le_output_dir.setText(self.output_dir) + + def runDependencyChecker(self): + # Temporarily apply the foam dir selection and paraview path selection + CfdTools.setFoamDir(self.foam_dir) + CfdTools.setParaviewPath(self.paraview_path) + CfdTools.setGmshPath(self.gmsh_path) + QApplication.setOverrideCursor(Qt.WaitCursor) + self.consoleMessage("Checking dependencies...") + msg = CfdTools.checkCfdDependencies() + self.consoleMessage(msg) + CfdTools.setFoamDir(self.initial_foam_dir) + CfdTools.setParaviewPath(self.initial_paraview_path) + CfdTools.setGmshPath(self.initial_gmsh_path) + QApplication.restoreOverrideCursor() + + def showAdministratorWarningMessage(self): + if platform.system() == "Windows": + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + if not is_admin: + button = QtGui.QMessageBox.question( + None, + "CfdOF Workbench", + "Before installing this software, it is advised to run FreeCAD in administrator mode (hold down " + " the 'Shift' key, right-click on the FreeCAD launcher, and choose 'Run as administrator').\n\n" + "If this is not possible, please make sure OpenFOAM is installed in a location to which you have " + "full read/write access rights.\n\n" + "You are not currently running as administrator - do you wish to continue anyway?") + return button == QtGui.QMessageBox.StandardButton.Yes + return True + + def downloadInstallOpenFoam(self): + if not self.showAdministratorWarningMessage(): + return + if self.createThread(): + self.thread.task = DOWNLOAD_OPENFOAM + self.thread.openfoam_url = self.form.le_openfoam_url.text() + self.thread.start() + + def pickOpenFoamFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose OpenFOAM install file', filter="*.exe") + if f and os.access(f, os.R_OK): + self.form.le_openfoam_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallParaview(self): + if self.createThread(): + self.thread.task = DOWNLOAD_PARAVIEW + self.thread.paraview_url = self.form.le_paraview_url.text() + self.thread.start() + + def pickParaviewFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView install file', filter="*.exe") + if f and os.access(f, os.R_OK): + self.form.le_paraview_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallCfMesh(self): + if not self.showAdministratorWarningMessage(): + return + + runtime = self.testGetRuntime(False) + if runtime == "MinGW" and self.form.le_cfmesh_url.text() == CFMESH_URL: + # Openfoam might have just been installed and the URL would not have had a chance to update + self.setDownloadURLs() + + if self.createThread(): + self.thread.task = DOWNLOAD_CFMESH + # We are forced to apply the foam dir selection - reset when the task finishes + CfdTools.setFoamDir(self.foam_dir) + self.thread.cfmesh_url = self.form.le_cfmesh_url.text() + self.thread.start() + + def pickCfMeshFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose cfMesh archive', filter="*.zip") + if f and os.access(f, os.R_OK): + self.form.le_cfmesh_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallHisa(self): + if not self.showAdministratorWarningMessage(): + return + + runtime = self.testGetRuntime(False) + if runtime == "MinGW" and self.form.le_hisa_url.text() == HISA_URL: + # Openfoam might have just been installed and the URL would not have had a chance to update + self.setDownloadURLs() + + if self.createThread(): + self.thread.task = DOWNLOAD_HISA + # We are forced to apply the foam dir selection - reset when the task finishes + CfdTools.setFoamDir(self.foam_dir) + self.thread.hisa_url = self.form.le_hisa_url.text() + self.thread.start() + + def pickHisaFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose HiSA archive', filter="*.zip") + if f and os.access(f, os.R_OK): + self.form.le_hisa_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def createThread(self): + if self.thread and self.thread.isRunning(): + self.consoleMessage("Busy - please wait...", 'Error') + return False + else: + self.thread = CfdPreferencePageThread() + self.thread.signals.error.connect(self.threadError) + self.thread.signals.finished.connect(self.threadFinished) + self.thread.signals.status.connect(self.threadStatus) + self.thread.signals.downloadProgress.connect(self.downloadProgress) + return True + + def threadStatus(self, msg): + self.consoleMessage(msg) + + def threadError(self, msg): + self.consoleMessage(msg, 'Error') + self.consoleMessage("Download unsuccessful") + + def threadFinished(self, status): + if self.thread.task == DOWNLOAD_CFMESH: + if status: + if CfdTools.getFoamRuntime() != "MinGW": + self.consoleMessage("Download completed") + user_dir = self.thread.user_dir + self.consoleMessage("Building cfMesh. Lengthy process - please wait...") + self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, CFMESH_FILE_BASE)) + if CfdTools.getFoamRuntime() == 'WindowsDocker': + # There seem to be issues when using multi processors to build in docker + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=1; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=`nproc`; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.consoleMessage("Install completed") + # Reset foam dir for now in case the user presses 'Cancel' + CfdTools.setFoamDir(self.initial_foam_dir) + elif self.thread.task == DOWNLOAD_HISA: + if status: + if CfdTools.getFoamRuntime() != "MinGW": + self.consoleMessage("Download completed") + user_dir = self.thread.user_dir + self.consoleMessage("Building HiSA. Please wait...") + self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, HISA_FILE_BASE)) + if CfdTools.getFoamRuntime() == 'WindowsDocker': + # There seem to be issues when using multi processors to build in docker + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=1; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=`nproc`; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.consoleMessage("Install completed") + # Reset foam dir for now in case the user presses 'Cancel' + CfdTools.setFoamDir(self.initial_foam_dir) + elif self.thread.task == DOWNLOAD_DOCKER: + if status: + self.consoleMessage("Download completed") + else: + self.consoleMessage("Download unsuccessful") + self.thread = None + + def installFinished(self, exit_code): + if exit_code: + self.consoleMessage("Install finished with error {}".format(exit_code)) + else: + self.consoleMessage("Install completed") + + def downloadProgress(self, bytes_done, bytes_total): + mb_done = float(bytes_done)/(1024*1024) + msg = "Downloaded {:.2f} MB".format(mb_done) + if bytes_total > 0: + msg += " of {:.2f} MB".format(float(bytes_total)/(1024*1024)) + self.form.labelDownloadProgress.setText(msg) + + def stderrFilter(self, text): + # Print to stdout rather than stderr so as not to alarm the user + # with the spurious wmkdep errors on stderr + print(text, end='') + return '' + + def dockerCheckboxClicked(self): + if CfdTools.docker_container==None: + CfdTools.docker_container = CfdTools.DockerContainer() + CfdTools.docker_container.usedocker = self.form.cb_docker_sel.isChecked() + self.form.pb_download_install_docker.setEnabled(CfdTools.docker_container.usedocker) + self.form.pb_download_install_openfoam.setEnabled(not CfdTools.docker_container.usedocker) + self.form.pb_download_install_hisa.setEnabled(not CfdTools.docker_container.usedocker) + self.form.pb_download_install_cfMesh.setEnabled(not CfdTools.docker_container.usedocker) + self.form.gb_docker.setVisible(CfdTools.docker_container.docker_cmd!=None or CfdTools.docker_container.usedocker) + + def downloadInstallDocker(self): + # Set foam dir and output dir in preparation for using docker + CfdTools.setFoamDir(self.form.le_foam_dir.text()) + self.saveSettings() + if self.createThread(): + self.thread.task = DOWNLOAD_DOCKER + self.thread.docker_url = self.form.le_docker_url.text() + self.thread.start() + +class CfdPreferencePageSignals(QObject): + error = QtCore.Signal(str) # Signal in PySide, pyqtSignal in PyQt + finished = QtCore.Signal(bool) + status = QtCore.Signal(str) + downloadProgress = QtCore.Signal(int, int) + + +class CfdPreferencePageThread(QThread): + """ Worker thread to complete tasks in preference page """ + def __init__(self): + super(CfdPreferencePageThread, self).__init__() + self.signals = CfdPreferencePageSignals() + self.quit = False + self.user_dir = None + self.task = None + self.openfoam_url = None + self.paraview_url = None + self.cfmesh_url = None + self.hisa_url = None + self.docker_url = None + + def run(self): + self.quit = False + + try: + if self.task == DOWNLOAD_OPENFOAM: + self.downloadOpenFoam() + elif self.task == DOWNLOAD_PARAVIEW: + self.downloadParaview() + elif self.task == DOWNLOAD_CFMESH: + self.downloadCfMesh() + elif self.task == DOWNLOAD_HISA: + self.downloadHisa() + elif self.task == DOWNLOAD_DOCKER: + self.downloadDocker() + except Exception as e: + if self.quit: + self.signals.finished.emit(False) # Exit quietly since UI already destroyed + return + else: + self.signals.error.emit(str(e)) + self.signals.finished.emit(False) + return + self.signals.finished.emit(True) + + def downloadFile(self, url, **kwargs): + block_size = kwargs.get('block_size', 10*1024) + context = kwargs.get('context', None) + reporthook = kwargs.get('reporthook', None) + suffix = kwargs.get('suffix', '') + with closing(urlrequest.urlopen(url, context=context)) as response: # For Python < 3.3 backward compatibility + download_len = int(response.info().get('Content-Length', 0)) + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp_file: + i = 0 + while True: + data = response.read(block_size) + if not data: + break + if self.quit: + raise RuntimeError("Premature termination received") + tmp_file.write(data) + i += 1 + if reporthook: + reporthook(i, block_size, download_len) + filename = tmp_file.name + return filename, response.info() + + def download(self, url, suffix, name): + self.signals.status.emit("Downloading {}, please wait...".format(name)) + try: + if hasattr(ssl, 'create_default_context'): + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + else: + context = None + # Download + (filename, header) = self.downloadFile( + url, suffix=suffix, reporthook=self.downloadStatus, context=context) + except Exception as ex: + raise Exception("Error downloading {}: {}".format(name, str(ex))) + + self.signals.status.emit("{} downloaded to {}".format(name, filename)) + return filename + + def downloadOpenFoam(self): + filename = self.download(self.openfoam_url, OPENFOAM_FILE_EXT, "OpenFOAM") + if QtCore.QProcess().startDetached(filename): + self.signals.status.emit("OpenFOAM installer launched - please complete the installation") + else: + raise Exception("Failed to launch OpenFOAM installer") + + def downloadParaview(self): + filename = self.download(self.paraview_url, PARAVIEW_FILE_EXT, "ParaView") + if QtCore.QProcess().startDetached(filename): + self.signals.status.emit("ParaView installer launched - please complete the installation") + else: + raise Exception("Failed to launch ParaView installer") + + def downloadCfMesh(self): + filename = self.download(self.cfmesh_url, CFMESH_FILE_EXT, "cfMesh") + + if CfdTools.getFoamRuntime() == "MinGW": + self.user_dir = None + self.signals.status.emit("Installing cfMesh...") + CfdTools.runFoamCommand( + '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. + format(CfdTools.translatePath(filename))) + else: + self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] + # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. + if CfdTools.getFoamRuntime() != 'WindowsDocker': + self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) + + self.signals.status.emit("Extracting cfMesh...") + if CfdTools.getFoamRuntime() == 'WindowsDocker': + from zipfile import ZipFile + with ZipFile(filename, 'r') as zip: + with tempfile.TemporaryDirectory() as tempdir: + zip.extractall(path=tempdir) + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' + .format(CfdTools.translatePath(os.path.join(tempdir, CFMESH_FILE_BASE)))) + else: + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. + format(CFMESH_FILE_BASE, CfdTools.translatePath(filename))) + + def downloadHisa(self): + filename = self.download(self.hisa_url, HISA_FILE_EXT, "HiSA") + + if CfdTools.getFoamRuntime() == "MinGW": + self.user_dir = None + self.signals.status.emit("Installing HiSA...") + CfdTools.runFoamCommand( + '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. + format(CfdTools.translatePath(filename))) + else: + self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] + # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. + if CfdTools.getFoamRuntime() != 'WindowsDocker': + self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) + + self.signals.status.emit("Extracting HiSA...") + if CfdTools.getFoamRuntime() == 'WindowsDocker': + from zipfile import ZipFile + with ZipFile(filename, 'r') as zip: + with tempfile.TemporaryDirectory() as tempdir: + zip.extractall(path=tempdir) + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' + .format(CfdTools.translatePath(os.path.join(tempdir, HISA_FILE_BASE)))) + else: + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. + format(HISA_FILE_BASE, CfdTools.translatePath(filename))) + + def downloadDocker(self): + self.signals.status.emit("Downloading Docker image, please wait until 'Download completed' message shown below") + if CfdTools.docker_container.container_id!=None: + CfdTools.docker_container.stop_container() + cmd = '{} pull {}'.format(CfdTools.docker_container.docker_cmd, self.docker_url) + if 'docker'in CfdTools.docker_container.docker_cmd: + cmd = cmd.replace('docker.io/','') + + CfdTools.runFoamCommand(cmd) + + def downloadStatus(self, blocks, block_size, total_size): + self.signals.downloadProgress.emit(blocks*block_size, total_size) diff --git a/CfdRemotePreferencePage.ui b/CfdRemotePreferencePage.ui new file mode 100644 index 00000000..34ee63a7 --- /dev/null +++ b/CfdRemotePreferencePage.ui @@ -0,0 +1,856 @@ + + + CfdRemotePreferencePage + + + + 0 + 0 + 488 + 1531 + + + + Remote Processing + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 12 + 75 + true + + + + + + + 1 + + + Use Remote Processing + + + + + + + + + + 1 + + + About Remote Processing + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + false + + + + + + + + 0 + 0 + + + + Add Profile + + + + + + + + 0 + 0 + + + + Delete Profile + + + + + + + + 75 + true + + + + Host profile: + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + + 75 + true + + + + Username on remote host: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Remote hostname or IP address: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Ping Remote Host + + + + + + + Test SSH + + + + + + + + + 8 + + + 8 + + + 8 + + + 0 + + + + + + 75 + true + + + + Meshing + + + Qt::AlignCenter + + + + + + + + 75 + true + + + + OpenFOAM + + + Qt::AlignCenter + + + + + + + + + 8 + + + 0 + + + 8 + + + 8 + + + + + Processes + + + + + + + Threads + + + + + + + + 0 + 0 + + + + + + + + Processes + + + + + + + Threads + + + + + + + + + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 14 + + + 6 + + + + + + 75 + true + + + + Remote output path + + + + + + + + + + true + + + The directory to which case folders are written. Used unless overridden on a per-analysis basis. + + + false + + + + + + + + 75 + true + + + + Remote OpenFOAM executable directory + + + + + + + ... + + + + + + + true + + + The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + false + + + + + + + true + + + The full path of the ParaView executable. Leave blank to use search path. + + + false + + + + + + + + 75 + true + + + + Remote gmsh executable path + + + + + + + true + + + ... + + + + + + + true + + + ... + + + + + + + true + + + ... + + + + + + + + 75 + true + + + + Remote ParaView executable path + + + + + + + Add filename to output path ? + + + + + + + + + 8 + + + 8 + + + 8 + + + 6 + + + 14 + + + 8 + + + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Remote Software dependencies + + + + + + + true + + + Run remote host dependency checker + + + + + + + + + + OpenFOAM + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + Install OpenFOAM + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + + + + + + ParaView + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + Install ParaView + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + + + + + + cfMesh + + + + + + + 0 + 0 + + + + URL: + + + + + + + Install cfMesh on remote host + + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + + + + + + + HiSA + + + + + + Install HiSA + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + + + + + + + Docker Container + + + + + + Use docker: + + + + + + + + + + Download from URL: + + + + + + + + + + Install Docker Container + + + + + + + + + + + + 8 + + + 5 + + + 8 + + + 8 + + + + + + 75 + true + + + + Output + + + + + + + + 0 + 0 + + + + + 0 + 120 + + + + QTextEdit::NoWrap + + + + + + + + + le_foam_dir + tb_choose_remote_foam_dir + le_paraview_path + tb_choose_paraview_path + le_gmsh_path + tb_choose_remote_gmsh_path + le_output_path + tb_choose_remote_output_dir + pb_run_dependency_checker + pb_download_install_openfoam + le_openfoam_url + tb_pick_openfoam_file + le_paraview_url + pb_download_install_paraview + tb_pick_paraview_file + le_cfmesh_url + pb_download_install_cfMesh + tb_pick_cfmesh_file + le_hisa_url + pb_download_install_hisa + tb_pick_hisa_file + + + + diff --git a/CfdTools.py b/CfdTools.py new file mode 100644 index 00000000..ff5cbcd9 --- /dev/null +++ b/CfdTools.py @@ -0,0 +1,2043 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2015 - Qingfeng Xia * +# * Copyright (c) 2017 Johan Heyns (CSIR) * +# * Copyright (c) 2017 Oliver Oxtoby (CSIR) * +# * Copyright (c) 2017 Alfred Bogaers (CSIR) * +# * Copyright (c) 2019-2022 Oliver Oxtoby * +# * Copyright (c) 2022 Jonathan Bergh * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 3 of the * +# * License, or (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with this program. If not, * +# * see . * +# * * +# *************************************************************************** + +# Utility functions like mesh exporting, shared by any CFD solver + +from __future__ import print_function + +import os +import os.path +import glob +import shutil +import tempfile +import numbers +import platform +import subprocess +import sys +import math +from datetime import timedelta +import FreeCAD +from FreeCAD import Units +import Part +import BOPTools +from BOPTools import SplitFeatures +from CfdOF.CfdConsoleProcess import CfdConsoleProcess +from CfdOF.CfdConsoleProcess import removeAppimageEnvironment +from PySide import QtCore + +# added for runCommand +from PySide.QtCore import QProcess # I added +from PySide.QtGui import QApplication # I added + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtGui + from PySide.QtGui import QFormLayout, QGridLayout + + +# Some standard install locations that are searched if an install directory is not specified +# Supports variable expansion and Unix-style globs (in which case the last lexically-sorted match will be used) +FOAM_DIR_DEFAULTS = {'Windows': ['C:\\Program Files\\ESI-OpenCFD\\OpenFOAM\\v*', + '~\\AppData\\Roaming\\ESI-OpenCFD\\OpenFOAM\\v*', + 'C:\\Program Files\\blueCFD-Core-*\\OpenFOAM-*'], + 'Linux': ['/usr/lib/openfoam/openfoam*', # ESI official packages + '/opt/openfoam*', '/opt/openfoam-dev', # Foundation official packages + '~/openfoam/OpenFOAM-v*', + '~/OpenFOAM/OpenFOAM-*.*', '~/OpenFOAM/OpenFOAM-dev'], # Typical self-built locations + "Darwin": ['~/OpenFOAM/OpenFOAM-*.*', '~/OpenFOAM/OpenFOAM-dev'] + } + +PARAVIEW_PATH_DEFAULTS = { + "Windows": ["C:\\Program Files\\ParaView *\\bin\\paraview.exe"], + "Linux": [], + "Darwin": [] + } + +QUANTITY_PROPERTIES = ['App::PropertyQuantity', + 'App::PropertyLength', + 'App::PropertyDistance', + 'App::PropertyAngle', + 'App::PropertyArea', + 'App::PropertyVolume', + 'App::PropertySpeed', + 'App::PropertyAcceleration', + 'App::PropertyForce', + 'App::PropertyPressure'] + +docker_container = None + +def getDefaultOutputPath(): + prefs = getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + if not output_path: + output_path = tempfile.gettempdir() + output_path = os.path.normpath(output_path) + return output_path + +def getDefaultRemoteOutputPath(): + prefs = getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultRemoteOutputPath", "") + if not output_path: + #TODO: fix this so it will run on the server + #should be /home/username from the reference page + # output_path = tempfile.gettempdir() + output_path = "" + # assume the remote computer is Linux and + # don't adjust the output path + # output_path = os.path.normpath(output_path) + return output_path + + +def getOutputPath(analysis): + if analysis and 'OutputPath' in analysis.PropertiesList: + output_path = analysis.OutputPath + else: + output_path = "" + if not output_path: + output_path = getDefaultOutputPath() + output_path = os.path.normpath(output_path) + return output_path + +def getRemoteOutputPath(analysis): + if analysis and 'RemoteOutputPath' in analysis.PropertiesList: + output_path = analysis.RemoteOutputPath + else: + output_path = "" + if not output_path: + output_path = getDefaultRemoteOutputPath() + # Assume the remote computer is Linux and don't + # adjust the path + # output_path = os.path.normpath(output_path) + return output_path + + +# Get functions +if FreeCAD.GuiUp: + def getResultObject(): + sel = FreeCADGui.Selection.getSelection() + if len(sel) == 1: + if sel[0].isDerivedFrom("Fem::FemResultObject"): + return sel[0] + for i in getActiveAnalysis().Group: + if i.isDerivedFrom("Fem::FemResultObject"): + return i + return None + + +def getParentAnalysisObject(obj): + """ + Return CfdAnalysis object to which this obj belongs in the tree + """ + from CfdOF import CfdAnalysis + parent = obj.getParentGroup() + if parent is None: + return None + elif hasattr(parent, 'Proxy') and isinstance(parent.Proxy, CfdAnalysis.CfdAnalysis): + return parent + else: + return getParentAnalysisObject(parent) + + +def getPhysicsModel(analysis_object): + is_present = False + for i in analysis_object.Group: + if "PhysicsModel" in i.Name: + physics_model = i + is_present = True + if not is_present: + physics_model = None + return physics_model + + +def getDynamicMeshAdaptation(analysis_object): + is_present = False + for i in getMesh(analysis_object).Group: + if "DynamicMeshInterfaceRefinement" in i.Name: + dynamic_mesh_adaption_model = i + is_present = True + if not is_present: + dynamic_mesh_adaption_model = None + return dynamic_mesh_adaption_model + + +def getMeshObject(analysis_object): + is_present = False + mesh_obj = [] + if analysis_object: + members = analysis_object.Group + else: + members = FreeCAD.activeDocument().Objects + from CfdOF.Mesh.CfdMesh import CfdMesh + for i in members: + if hasattr(i, "Proxy") and isinstance(i.Proxy, CfdMesh): + if is_present: + FreeCAD.Console.PrintError("Analysis contains more than one mesh object.") + else: + mesh_obj.append(i) + is_present = True + if not is_present: + mesh_obj = [None] + return mesh_obj[0] + + +def getPorousZoneObjects(analysis_object): + return [i for i in analysis_object.Group if i.Name.startswith('PorousZone')] + + +def getInitialisationZoneObjects(analysis_object): + return [i for i in analysis_object.Group if i.Name.startswith('InitialisationZone')] + + +def getZoneObjects(analysis_object): + return [i for i in analysis_object.Group if 'Zone' in i.Name] + + +def getInitialConditions(analysis_object): + from CfdOF.Solve.CfdInitialiseFlowField import CfdInitialVariables + for i in analysis_object.Group: + if isinstance(i.Proxy, CfdInitialVariables): + return i + return None + + +def getMaterials(analysis_object): + return [i for i in analysis_object.Group if i.isDerivedFrom('App::MaterialObjectPython')] + + +def getSolver(analysis_object): + from CfdOF.Solve.CfdSolverFoam import CfdSolverFoam + for i in analysis_object.Group: + if isinstance(i.Proxy, CfdSolverFoam): + return i + + +def getSolverSettings(solver): + """ + Convert properties into python dict, while key must begin with lower letter. + """ + dict = {} + f = lambda s: s[0].lower() + s[1:] + for prop in solver.PropertiesList: + dict[f(prop)] = solver.getPropertyByName(prop) + return dict + + +def getCfdBoundaryGroup(analysis_object): + group = [] + from CfdOF.Solve.CfdFluidBoundary import CfdFluidBoundary + for i in analysis_object.Group: + if isinstance(i.Proxy, CfdFluidBoundary): + group.append(i) + return group + + +def isPlanar(shape): + """ + Return whether the shape is a planar face + """ + n = shape.normalAt(0.5, 0.5) + if len(shape.Vertexes) <= 3: + return True + for v in shape.Vertexes[1:]: + t = v.Point - shape.Vertexes[0].Point + c = t.dot(n) + if c / t.Length > 1e-8: + return False + return True + + +def getMesh(analysis_object): + from CfdOF.Mesh.CfdMesh import CfdMesh + for i in analysis_object.Group: + if hasattr(i, "Proxy") and isinstance(i.Proxy, CfdMesh): + return i + return None + + +def getResult(analysis_object): + for i in analysis_object.Group: + if i.isDerivedFrom("Fem::FemResultObject"): + return i + return None + + +def getModulePath(): + """ + Returns the current Cfd module path. + Determines where this file is running from, so works regardless of whether + the module is installed in the app's module directory or the user's app data folder. + (The second overrides the first.) + """ + return os.path.normpath(os.path.join(os.path.dirname(__file__), os.path.pardir)) + + +# Function objects +def getReportingFunctionsGroup(analysis_object): + group = [] + from CfdOF.PostProcess.CfdReportingFunction import CfdReportingFunction + for i in analysis_object.Group: + if isinstance(i.Proxy, CfdReportingFunction): + group.append(i) + return group + + +def getScalarTransportFunctionsGroup(analysis_object): + group = [] + from CfdOF.Solve.CfdScalarTransportFunction import CfdScalarTransportFunction + for i in analysis_object.Group: + if isinstance(i.Proxy, CfdScalarTransportFunction): + group.append(i) + return group + + +# Mesh +def getMeshRefinementObjs(mesh_obj): + from CfdOF.Mesh.CfdMeshRefinement import CfdMeshRefinement + ref_objs = [] + for obj in mesh_obj.Group: + if hasattr(obj, "Proxy") and isinstance(obj.Proxy, CfdMeshRefinement): + ref_objs = ref_objs + [obj] + return ref_objs + + +# Set functions +def setCompSolid(vobj): + """ + To enable correct mesh refinement, boolean fragments are set to compSolid mode + """ + doc_name = str(vobj.Object.Document.Name) + doc = FreeCAD.getDocument(doc_name) + for obj in doc.Objects: + if hasattr(obj, 'Proxy') and isinstance(obj.Proxy, BOPTools.SplitFeatures.FeatureBooleanFragments): + FreeCAD.getDocument(doc_name).getObject(obj.Name).Mode = 'CompSolid' + + +def normalise(v): + import numpy + mag = numpy.sqrt(sum(vi**2 for vi in v)) + import sys + if mag < sys.float_info.min: + mag += sys.float_info.min + return [vi/mag for vi in v] + + +def cfdMessage(msg): + """ + Print a message to console and refresh GUI + """ + FreeCAD.Console.PrintMessage(msg) + if FreeCAD.GuiUp: + FreeCAD.Gui.updateGui() + FreeCAD.Gui.updateGui() + + +def cfdWarning(msg): + """ + Print a message to console and refresh GUI + """ + FreeCAD.Console.PrintWarning(msg) + if FreeCAD.GuiUp: + FreeCAD.Gui.updateGui() + FreeCAD.Gui.updateGui() + + +def cfdError(msg): + """ + Print a message to console and refresh GUI + """ + FreeCAD.Console.PrintError(msg) + if FreeCAD.GuiUp: + FreeCAD.Gui.updateGui() + FreeCAD.Gui.updateGui() + + +def cfdErrorBox(msg): + """ + Show message for an expected error + """ + QtGui.QApplication.restoreOverrideCursor() + if FreeCAD.GuiUp: + QtGui.QMessageBox.critical(None, "CfdOF Workbench", msg) + else: + FreeCAD.Console.PrintError(msg + "\n") + + +def formatTimer(seconds): + """ + Put the elapsed time printout into a nice format + """ + return str(timedelta(seconds=seconds)).split('.', 2)[0].lstrip('0').lstrip(':') + + +def getColour(type): + """ + type: 'Error', 'Warning', 'Logging', 'Text' + """ + col_int = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/OutputWindow").GetUnsigned('color'+type) + return '#{:08X}'.format(col_int)[:-2] + + +def setQuantity(inputField, quantity): + """ + Set the quantity (quantity object or unlocalised string) into the inputField correctly + """ + # Must set in the correctly localised value as the user would enter it. + # A bit painful because the python locale settings seem to be based on language, + # not input settings as the FreeCAD settings are. So can't use that; hence + # this rather roundabout way involving the UserString of Quantity + q = Units.Quantity(quantity) + # Avoid any truncation + if isinstance(q.Format, tuple): # Backward compat + q.Format = (12, 'e') + else: + q.Format = {'Precision': 12, 'NumberFormat': 'e', 'Denominator': q.Format['Denominator']} + inputField.setProperty("quantityString", q.UserString) + + +def getQuantity(inputField): + """ + Get the quantity as an unlocalised string from an inputField + """ + q = inputField.property("quantity") + return str(q) + + +def indexOrDefault(list, findItem, defaultIndex): + """ + Look for findItem in list, and return defaultIndex if not found + """ + try: + return list.index(findItem) + except ValueError: + return defaultIndex + + +def storeIfChanged(obj, prop, val): + cur_val = getattr(obj, prop) + if isinstance(cur_val, Units.Quantity): + if str(cur_val) != str(val): + FreeCADGui.doCommand("App.ActiveDocument.{}.{} = '{}'".format(obj.Name, prop, val)) + elif cur_val != val: + if isinstance(cur_val, str): + FreeCADGui.doCommand("App.ActiveDocument.{}.{} = '{}'".format(obj.Name, prop, val)) + elif isinstance(cur_val, FreeCAD.Vector): + FreeCADGui.doCommand("App.ActiveDocument.{}.{} = App.{}".format(obj.Name, prop, val)) + else: + FreeCADGui.doCommand("App.ActiveDocument.{}.{} = {}".format(obj.Name, prop, val)) + + +def copyFilesRec(src, dst, symlinks=False, ignore=None): + """ + Recursively copy files from src dir to dst dir + """ + if not os.path.exists(dst): + os.makedirs(dst) + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if not os.path.isdir(s): + shutil.copy2(s, d) + + +def getPatchType(bcType, bcSubType): + """ + Get the boundary type based on selected BC condition + """ + if bcType == 'wall': + return 'wall' + elif bcType == 'empty': + return 'empty' + elif bcType == 'constraint': + if bcSubType == 'symmetry': + return 'symmetry' + elif bcSubType == 'cyclic': + return 'cyclic' + elif bcSubType == 'wedge': + return 'wedge' + elif bcSubType == 'empty': + return 'empty' + else: + return 'patch' + else: + return 'patch' + + +def movePolyMesh(case): + """ + Move polyMesh to polyMesh.org to ensure availability if cleanCase is ran from the terminal. + """ + meshOrg_dir = case + os.path.sep + "constant/polyMesh.org" + mesh_dir = case + os.path.sep + "constant/polyMesh" + if os.path.isdir(meshOrg_dir): + shutil.rmtree(meshOrg_dir) + shutil.copytree(mesh_dir, meshOrg_dir) + shutil.rmtree(mesh_dir) + + +def getPreferencesLocation(): + # Set parameter location + return "User parameter:BaseApp/Preferences/Mod/CfdOF" + +def setFoamDir(installation_path): + prefs = getPreferencesLocation() + # Set OpenFOAM install path in parameters + FreeCAD.ParamGet(prefs).SetString("InstallationPath", installation_path) + +#not used anymore +def setRemoteFoamDir(remote_installation_path): + print("Error: setRemoteFoamDir is depreciated.") + prefs = getPreferencesLocation() + # Set OpenFOAM remote install path in parameters + FreeCAD.ParamGet(prefs).SetString("RemoteInstallationPath", remote_installation_path) + +# not used anymore +def getRemoteFoamDir(): + print("Error: getRemoteFoamDir is depreciated.") + prefs = getPreferencesLocation() + # Set OpenFOAM remote install path in parameters + return FreeCAD.ParamGet(prefs).GetString("RemoteInstallationPath", "") + +def startDocker(): + global docker_container + if docker_container==None: + docker_container = DockerContainer() + if docker_container.container_id==None: + if "podman" in docker_container.docker_cmd: + # Start podman machine if not already started + exit_code = checkPodmanMachineRunning() + if exit_code==2: + startPodmanMachine() + if checkPodmanMachineRunning(): + print("Aborting docker container initialization") + return 1 + elif exit_code==1: + print("Aborting docker container initialization") + return 1 + docker_container.start_container() + if docker_container.container_id != None: + print("Docker image {} started. ID = {}".format(docker_container.image_name, docker_container.container_id)) + return 0 + else: + print("Docker start appears to have failed") + return 1 + +def checkPodmanMachineRunning(): + print("Checking podman machine running") + cmd = "podman machine list" + proc = QtCore.QProcess() + proc.start(cmd) + proc.waitForFinished() + line = "" + while proc.canReadLine(): + line = str(proc.readLine().data(), encoding="utf-8") + if len(line)>0: + print(line) + if "Currently running" in line: + print("Podman machine running") + return 0 + elif line[-9:]=="DISK SIZE": + print("Podman machine not initialized - please refer to readme") + return 1 + else: + print("Podman machine not running") + return 2 + +def startPodmanMachine(): + print("Attempting podman machine start") + cmd = "podman machine start" + proc = QtCore.QProcess() + proc.start(cmd) + proc.waitForFinished() + line = "" + while proc.canReadLine(): + line = str(proc.readLine().data(), encoding="utf-8") + if len(line)>0: + print(line) + cmd = "podman machine set --rootful" + proc = QtCore.QProcess() + proc.start(cmd) + proc.waitForFinished() + line = "" + while proc.canReadLine(): + line = str(proc.readLine().data(), encoding="utf-8") + if len(line)>0: + print(line) + +def getFoamDir(): + global docker_container + if docker_container==None: + docker_container = DockerContainer() + if docker_container.usedocker: + return "" + + prefs = getPreferencesLocation() + # Get OpenFOAM install path from parameters + installation_path = FreeCAD.ParamGet(prefs).GetString("InstallationPath", "") + # Ensure parameters exist for future editing + setFoamDir(installation_path) + + # If not specified, try to detect from shell environment settings and defaults + if not installation_path: + installation_path = detectFoamDir() + + if installation_path: + installation_path = os.path.normpath(installation_path) + + return installation_path + + +def getFoamRuntime(): + global docker_container + if docker_container==None: + docker_container = DockerContainer() + if docker_container.usedocker: + return 'PosixDocker' + + installation_path = getFoamDir() + if installation_path is None: + raise IOError("OpenFOAM installation path not set and not detected") + + runtime = None + if platform.system() == 'Windows': + if os.path.exists(os.path.join(installation_path, "msys64", "home", "ofuser", ".blueCFDCore")): + runtime = 'BlueCFD' + elif os.path.exists(os.path.join(installation_path, "..", "msys64", "home", "ofuser", ".blueCFDCore")): + runtime = 'BlueCFD2' + elif os.path.exists(os.path.join(installation_path, "msys64", "home", "ofuser")): + runtime = 'MinGW' + elif os.path.exists(os.path.join(installation_path, "Windows", "Scripts")): + runtime = 'WindowsDocker' + elif os.path.exists(os.path.join(getFoamDir(), "etc", "bashrc")): + runtime = 'BashWSL' + else: + if not len(getFoamDir()): + runtime = 'PosixPreloaded' + if os.path.exists(os.path.join(getFoamDir(), "etc", "bashrc")): + runtime = 'Posix' + + if not runtime: + raise IOError("The directory {} is not a recognised OpenFOAM installation".format(installation_path)) + + return runtime + + +def findInDefaultPaths(paths): + for d in paths.get(platform.system(), []): + d = glob.glob(os.path.expandvars(os.path.expanduser(d))) + if len(d): + d = sorted(d)[-1] + if os.path.exists(d): + return d + return None + + +def detectFoamDir(): + """ + Try to guess Foam install dir from WM_PROJECT_DIR or, failing that, various defaults + """ + foam_dir = None + if platform.system() == "Linux": + # Detect pre-loaded environment + cmdline = ['bash', '-l', '-c', 'echo $WM_PROJECT_DIR'] + foam_dir = subprocess.check_output(cmdline, stderr=subprocess.PIPE, universal_newlines=True) + if len(foam_dir) > 1: # If env var is not defined, `\n` returned + foam_dir = foam_dir.strip() # Python2: Strip EOL char + else: + foam_dir = None + if foam_dir and not os.path.exists(os.path.join(foam_dir, "etc", "bashrc")): + foam_dir = None + if not foam_dir: + foam_dir = None + + if foam_dir is None: + foam_dir = findInDefaultPaths(FOAM_DIR_DEFAULTS) + return foam_dir + + +def setParaviewPath(paraview_path): + prefs = getPreferencesLocation() + # Set Paraview install path in parameters + FreeCAD.ParamGet(prefs).SetString("ParaviewPath", paraview_path) + +def getParaviewPath(): + prefs = getPreferencesLocation() + # Get path from parameters + paraview_path = FreeCAD.ParamGet(prefs).GetString("ParaviewPath", "") + # Ensure parameters exist for future editing + setParaviewPath(paraview_path) + return paraview_path + + +def setGmshPath(gmsh_path): + prefs = getPreferencesLocation() + # Set Paraview install path in parameters + FreeCAD.ParamGet(prefs).SetString("GmshPath", gmsh_path) + +# not used anymore +def setRemoteGmshPath(gmsh_path): + print("Error: setRemoteGmshPath is depreciated.") + prefs = getPreferencesLocation() + # Set Paraview install path in parameters + FreeCAD.ParamGet(prefs).SetString("RemoteGmshPath", gmsh_path) + +def getGmshPath(): + prefs = getPreferencesLocation() + # Get path from parameters + gmsh_path = FreeCAD.ParamGet(prefs).GetString("GmshPath", "") + # Ensure parameters exist for future editing + setGmshPath(gmsh_path) + return gmsh_path + +# not used anymore +def getRemoteGmshPath(): + print("Error: getRemoteGmshPath is depreciated.") + prefs = getPreferencesLocation() + # Get path from parameters + gmsh_path = FreeCAD.ParamGet(prefs).GetString("RemoteGmshPath", "") + # Ensure parameters exist for future editing + setGmshPath(gmsh_path) + return gmsh_path + + +def translatePath(p): + """ + Transform path to the perspective of the Linux subsystem in which OpenFOAM is run (e.g. mingw) + """ + if platform.system() == 'Windows': + return fromWindowsPath(p) + else: + return p + + +def reverseTranslatePath(p): + """ + Transform path from the perspective of the OpenFOAM subsystem to the host system + """ + if platform.system() == 'Windows': + return toWindowsPath(p) + else: + return p + + +def fromWindowsPath(p): + drive, tail = os.path.splitdrive(p) + pp = tail.replace('\\', '/') + if getFoamRuntime() == "MinGW" or getFoamRuntime() == "BlueCFD" or getFoamRuntime() == "BlueCFD2": + # Under mingw: c:\path -> /c/path + if os.path.isabs(p): + return "/" + (drive[:-1]).lower() + pp + else: + return pp + elif getFoamRuntime() == "WindowsDocker": + # Under docker: / -> /home/ofuser/workingDir/ + if os.path.isabs(p): + homepath = os.path.expanduser('~') + try: + if os.path.commonpath((os.path.normpath(p), homepath)) == homepath: + return '/home/ofuser/workingDir/' + os.path.relpath(p, homepath).replace('\\', '/') + else: + raise ValueError("The path {} is not inside the users's home directory.".format(p)) + except ValueError: + cfdError( + "The path {} cannot be used in the Docker environment. " + "Only paths inside the user's home directory are accessible.".format(p)) + raise + else: + return pp + elif getFoamRuntime() == "BashWSL": + # bash on windows: C:\Path -> /mnt/c/Path + if os.path.isabs(p): + return "/mnt/" + (drive[:-1]).lower() + pp + else: + return pp + else: # Nothing needed for posix + return p + + +def toWindowsPath(p): + pp = p.split('/') + if getFoamRuntime() == "MinGW": + # Under mingw: /c/path -> c:\path; /home/ofuser -> /msys64/home/ofuser + if p.startswith('/home/ofuser'): + return getFoamDir() + '\\msys64\\home\\ofuser\\' + '\\'.join(pp[3:]) + elif p.startswith('/'): + return pp[1].upper() + ':\\' + '\\'.join(pp[2:]) + else: + return p.replace('/', '\\') + elif getFoamRuntime() == "WindowsDocker": + # Under docker: /home/ofuser/workingDir/ -> / + homepath = os.path.expanduser('~') + if p.startswith('/home/ofuser/workingDir/'): + return os.path.join(homepath, "\\".join(pp[4:])) + else: + return p.replace('/', '\\') + elif getFoamRuntime() == "BashWSL": + # bash on windows: /mnt/c/Path -> C:\Path + if p.startswith('/mnt/'): + return pp[2].toupper() + ':\\' + '\\'.join(pp[3:]) + else: + return p.replace('/', '\\') + elif getFoamRuntime().startswith("BlueCFD"): + # Under blueCFD (mingw): /c/path -> c:\path; /home/ofuser/blueCFD -> + if p.startswith('/home/ofuser/blueCFD'): + if getFoamRuntime() == "BlueCFD2": + foam_dir = getFoamDir() + '\\' + '..' + else: + foam_dir = getFoamDir() + return foam_dir + '\\' + '\\'.join(pp[4:]) + elif p.startswith('/'): + return pp[1].upper() + ':\\' + '\\'.join(pp[2:]) + else: + return p.replace('/', '\\') + else: # Nothing needed for posix + return p + + +def getShortWindowsPath(long_name): + """ + Gets the short path name of a given long path. http://stackoverflow.com/a/23598461/200291 + """ + import ctypes + from ctypes import wintypes + _GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW + _GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD] + _GetShortPathNameW.restype = wintypes.DWORD + + output_buf_size = 0 + while True: + output_buf = ctypes.create_unicode_buffer(output_buf_size) + needed = _GetShortPathNameW(os.path.normpath(long_name), output_buf, output_buf_size) + if output_buf_size >= needed: + return output_buf.value + else: + output_buf_size = needed + + +def getRunEnvironment(): + """ + Return native environment settings necessary for running on relevant platform + """ + if getFoamRuntime() == "MinGW": + return {"MSYSTEM": "MSYS", + "USERNAME": "ofuser", + "USER": "ofuser", + "HOME": "/home/ofuser"} + elif getFoamRuntime().startswith("BlueCFD"): + return {"MSYSTEM": "MINGW64", + "USERNAME": "ofuser", + "USER": "ofuser", + "HOME": "/home/ofuser"} + else: + return {} + + +def makeRunCommand(cmd, dir, source_env=True): + """ + Generate native command to run the specified Linux command in the relevant environment, + including changing to the specified working directory if applicable + """ + + if getFoamRuntime() == "PosixDocker" and ' pull ' in cmd: + # Case where running from Install Docker thread + return cmd.split() + + installation_path = getFoamDir() + if installation_path is None: + raise IOError("OpenFOAM installation directory not found") + + FreeCAD.Console.PrintMessage('Executing: {} in {}\n'.format(cmd, dir)) + + source = "" + if source_env and len(installation_path): + env_setup_script = "{}/etc/bashrc".format(installation_path) + source = 'source "{}" && '.format(env_setup_script) + + if getFoamRuntime() == "PosixDocker": + # Set source for docker container + source = 'source /etc/bashrc && ' + + cd = "" + if dir: + cd = 'cd "{}" && '.format(translatePath(dir)) + + if getFoamRuntime() == "PosixDocker": + prefs = getPreferencesLocation() + if dir: + cd = cd.replace(FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", ""),'/tmp').replace('\\','/') + + if getFoamRuntime() == "MinGW": + # .bashrc will exit unless shell is interactive, so we have to manually load the foam bashrc + foamVersion = os.path.split(installation_path)[-1].lstrip('v') + cmdline = ['{}\\msys64\\usr\\bin\\bash'.format(installation_path), '--login', '-O', 'expand_aliases', '-c', + 'echo Sourcing OpenFOAM environment...; ' + 'source $HOME/OpenFOAM/OpenFOAM-v{}/etc/bashrc; '.format(foamVersion) + + 'export PATH=$FOAM_LIBBIN/msmpi:$FOAM_LIBBIN:$WM_THIRD_PARTY_DIR/platforms/linux64MingwDPInt32/lib:$PATH; ' + + cd + cmd] + return cmdline + + if getFoamRuntime() == "PosixDocker": + global docker_container + if docker_container.output_path_used!=FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", ""): + print("Output path changed - restarting container") + docker_container.stop_container() + docker_container.start_container() + if platform.system() == 'Windows' and FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "")[:5]=='\\\\wsl' and cmd[:5] == './All': + cmd = 'chmod 744 {0} && {0}'.format(cmd) # If using windows wsl$ output directory, need to make the command executable + cmdline = [docker_container.docker_cmd, 'exec', docker_container.container_id, 'bash', '-c', source + cd + cmd] + return cmdline + + if getFoamRuntime() == "WindowsDocker": + foamVersion = os.path.split(installation_path)[-1].lstrip('v') + cmdline = ['powershell.exe', + 'docker-machine.exe start default; ' + 'docker-machine.exe env --shell powershell default | Invoke-Expression; ' + 'docker start of_{}; '.format(foamVersion) + + 'docker exec --privileged of_{} '.format(foamVersion) + + 'bash -c "su -c \'' + # $ -> `$: escaping for powershell + (cd + cmd).replace('$', '`$').replace('"', '\\`"') + # Escape quotes for powershell and also cmdline to bash + '\' -l ofuser"'] + return cmdline + elif getFoamRuntime() == "BashWSL": + cmdline = ['bash', '-c', source + cd + cmd] + return cmdline + elif getFoamRuntime().startswith("BlueCFD"): + # Set-up necessary for running a command - only needs doing once, but to be safe... + if getFoamRuntime() == "BlueCFD2": + inst_path = "{}\\..".format(installation_path) + else: + inst_path = "{}".format(installation_path) + short_bluecfd_path = getShortWindowsPath(inst_path) + with open('{}\\msys64\\home\\ofuser\\.blueCFDOrigin'.format(inst_path), "w") as f: + f.write(short_bluecfd_path) + f.close() + srcdir = '{}\\msys64\\mingw64\\bin'.format(inst_path) + destdir1 = None + destdir2 = None + with os.scandir('{}'.format(inst_path)) as dirs: + for dir in dirs: + if dir.is_dir() and dir.name.startswith('OpenFOAM-'): + destdir1 = os.path.join(inst_path, dir.name, 'platforms\\mingw_w64GccDPInt32Opt\\bin') + if dir.is_dir() and dir.name.startswith('ofuser-of'): + destdir2 = os.path.join(inst_path, dir.name, 'platforms\\mingw_w64GccDPInt32Opt\\bin') + if not destdir1 or not destdir2: + cfdError("Unable to find directories 'OpenFOAM-*' and 'ofuser-of*' in path {}. " + "Possible error in BlueCFD installation.".format(inst_path)) + try: + file = 'libstdc++-6.dll' + if destdir1 and not os.path.isfile(os.path.join(destdir1, file)): + shutil.copy(os.path.join(srcdir, file), os.path.join(destdir1, file)) + if destdir2 and not os.path.isfile(os.path.join(destdir2, file)): + shutil.copy(os.path.join(srcdir, file), os.path.join(destdir2, file)) + file = 'libgomp-1.dll' + if not os.path.isfile(os.path.join(destdir1, file)): + shutil.copy(os.path.join(srcdir, file), os.path.join(destdir1, file)) + if not os.path.isfile(os.path.join(destdir2, file)): + shutil.copy(os.path.join(srcdir, file), os.path.join(destdir2, file)) + except IOError as err: + cfdError("Unable to copy file {} from directory {} to {} and {}: {}\n" + "Try running FreeCAD again with administrator privileges, or copy the file manually." + .format(file, srcdir, destdir1, destdir2, str(err))) + + # Note: Prefixing bash call with the *short* path can prevent errors due to spaces in paths + # when running linux tools - specifically when building + cmdline = ['{}\\msys64\\usr\\bin\\bash'.format(short_bluecfd_path), '--login', '-O', 'expand_aliases', '-c', + cd + cmd] + return cmdline + else: + cmdline = ['bash', '-c', source + cd + cmd] + return cmdline + + +def runFoamCommand(cmdline, case=None): + """ + Run a command in the OpenFOAM environment and wait until finished. Return output as (stdout, stderr, combined) + Also print output as we go. + cmdline - The command line to run as a string + e.g. transformPoints -scale "(0.001 0.001 0.001)" + case - Case directory or path + """ + proc = CfdSynchronousFoamProcess() + exit_code = proc.run(cmdline, case) + # Reproduce behaviour of failed subprocess run + if exit_code: + raise subprocess.CalledProcessError(exit_code, cmdline) + return proc.output, proc.outputErr, proc.outputAll + + + +def startFoamApplication(cmd, case, log_name='', finished_hook=None, stdout_hook=None, stderr_hook=None): + """ + Run command cmd in OpenFOAM environment, sending output to log file. + Returns a CfdConsoleProcess object after launching + cmd - List or string with the application being the first entry followed by the options. + e.g. ['transformPoints', '-scale', '"(0.001 0.001 0.001)"'] + case - Case path + log_name - File name to pipe output to, if not None. If zero-length string, will generate automatically + as log. where is the first element in cmd. + """ + if isinstance(cmd, list) or isinstance(cmd, tuple): + cmds = cmd + elif isinstance(cmd, str): + cmds = cmd.split(' ') # Insensitive to incorrect split like space and quote + else: + raise Exception("Error: Application and options must be specified as a list or tuple.") + + if log_name == '': + app = cmds[0].rsplit('/', 1)[-1] + logFile = "log.{}".format(app) + else: + logFile = log_name + + cmdline = ' '.join(cmds) # Space to separate options + # Pipe to log file and terminal + if logFile: + cmdline += " 1> >(tee -a " + logFile + ") 2> >(tee -a " + logFile + " >&2)" + # Tee appends to the log file, so we must remove first. Can't do directly since + # paths may be specified using variables only available in foam runtime environment. + cmdline = "{{ rm -f {}; {}; }}".format(logFile, cmdline) + + proc = CfdConsoleProcess(finished_hook=finished_hook, stdout_hook=stdout_hook, stderr_hook=stderr_hook) + if logFile: + print("Running ", ' '.join(cmds), " -> ", logFile) + else: + print("Running ", ' '.join(cmds)) + + proc.start(makeRunCommand(cmdline, case), env_vars=getRunEnvironment()) + if not proc.waitForStarted(): + raise Exception("Unable to start command " + ' '.join(cmds)) + return proc + + +def runFoamApplication(cmd, case, log_name=''): + """ + Same as startFoamApplication, but waits until complete. Returns exit code. + """ + proc = startFoamApplication(cmd, case, log_name) + proc.waitForFinished() + return proc.exitCode() + + +def convertMesh(case, mesh_file, scale): + """ + Convert gmsh created UNV mesh to FOAM. A scaling of 1e-3 is prescribed as the CAD is always in mm while FOAM + uses SI units (m). + """ + + if mesh_file.find(".unv") > 0: + mesh_file = translatePath(mesh_file) + cmdline = ['ideasUnvToFoam', '"{}"'.format(mesh_file)] + runFoamApplication(cmdline, case) + # changeBoundaryType(case, 'defaultFaces', 'wall') # rename default boundary type to wall + # Set in the correct patch types + cmdline = ['changeDictionary'] + runFoamApplication(cmdline, case) + else: + raise Exception("Error: Only supporting unv mesh files.") + + if scale and isinstance(scale, numbers.Number): + cmdline = ['transformPoints', '-scale', '"({} {} {})"'.format(scale, scale, scale)] + runFoamApplication(cmdline, case) + else: + print("Error: mesh scaling ratio is must be a float or integer\n") + + +def checkCfdDependencies(): + FC_MAJOR_VER_REQUIRED = 0 + FC_MINOR_VER_REQUIRED = 18 + FC_PATCH_VER_REQUIRED = 4 + FC_COMMIT_REQUIRED = 16146 + + CF_MAJOR_VER_REQUIRED = 1 + CF_MINOR_VER_REQUIRED = 16 + + HISA_MAJOR_VER_REQUIRED = 1 + HISA_MINOR_VER_REQUIRED = 6 + HISA_PATCH_VER_REQUIRED = 4 + + message = "" + FreeCAD.Console.PrintMessage("Checking CFD workbench dependencies...\n") + + # Check FreeCAD version + print("Checking FreeCAD version") + ver = FreeCAD.Version() + major_ver = int(ver[0]) + minor_vers = ver[1].split('.') + minor_ver = int(minor_vers[0]) + if minor_vers[1:] and minor_vers[1]: + patch_ver = int(minor_vers[1]) + else: + patch_ver = 0 + gitver = ver[2].split() + if gitver: + gitver = gitver[0] + if gitver and gitver != 'Unknown': + gitver = int(gitver) + else: + # If we don't have the git version, assume it's OK. + gitver = FC_COMMIT_REQUIRED + + if (major_ver < FC_MAJOR_VER_REQUIRED or + (major_ver == FC_MAJOR_VER_REQUIRED and + (minor_ver < FC_MINOR_VER_REQUIRED or + (minor_ver == FC_MINOR_VER_REQUIRED and + (patch_ver < FC_PATCH_VER_REQUIRED or + (patch_ver == FC_PATCH_VER_REQUIRED and + gitver < FC_COMMIT_REQUIRED)))))): + fc_msg = "FreeCAD version ({}.{}.{}) ({}) must be at least {}.{}.{} ({})".format( + int(ver[0]), minor_ver, patch_ver, gitver, + FC_MAJOR_VER_REQUIRED, FC_MINOR_VER_REQUIRED, FC_PATCH_VER_REQUIRED, FC_COMMIT_REQUIRED) + print(fc_msg) + message += fc_msg + '\n' + + # check openfoam + print("Checking for OpenFOAM:") + try: + foam_dir = getFoamDir() + sys_msg = "System: {}\nRuntime: {}\nOpenFOAM directory: {}".format( + platform.system(), getFoamRuntime(), foam_dir if len(foam_dir) else "(system installation)") + print(sys_msg) + message += sys_msg + '\n' + except IOError as e: + ofmsg = "Could not find OpenFOAM installation: " + str(e) + print(ofmsg) + message += ofmsg + '\n' + else: + if foam_dir is None: + ofmsg = "OpenFOAM installation path not set and OpenFOAM environment neither pre-loaded before " + \ + "running FreeCAD nor detected in standard locations" + print(ofmsg) + message += ofmsg + '\n' + else: + if getFoamRuntime() == "PosixDocker": + startDocker() + try: + if getFoamRuntime() == "MinGW": + foam_ver = runFoamCommand("echo $FOAM_API")[0] + else: + foam_ver = runFoamCommand("echo $WM_PROJECT_VERSION")[0] + except Exception as e: + runmsg = "OpenFOAM installation found, but unable to run command: " + str(e) + message += runmsg + '\n' + print(runmsg) + raise + else: + foam_ver = foam_ver.rstrip() + if foam_ver: + foam_ver = foam_ver.split()[-1] + if foam_ver and foam_ver != 'dev' and foam_ver != 'plus': + try: + # Isolate major version number + foam_ver = foam_ver.lstrip('v') + foam_ver = int(foam_ver.split('.')[0]) + if getFoamRuntime() == "MinGW": + if foam_ver < 2012 or foam_ver > 2206: + vermsg = "OpenFOAM version " + str(foam_ver) + \ + " is not currently supported with MinGW installation" + message += vermsg + "\n" + print(vermsg) + if foam_ver >= 1000: # Plus version + if foam_ver < 1706: + vermsg = "OpenFOAM version " + str(foam_ver) + " is outdated:\n" + \ + "Minimum version 1706 or 5 required" + message += vermsg + "\n" + print(vermsg) + if foam_ver > 2206: + vermsg = "OpenFOAM version " + str(foam_ver) + " is not yet supported:\n" + \ + "Last tested version is 2206" + message += vermsg + "\n" + print(vermsg) + else: # Foundation version + if foam_ver < 5: + vermsg = "OpenFOAM version " + str(foam_ver) + " is outdated:\n" + \ + "Minimum version 5 or 1706 required" + message += vermsg + "\n" + print(vermsg) + if foam_ver > 9: + vermsg = "OpenFOAM version " + str(foam_ver) + " is not yet supported:\n" + \ + "Last tested version is 9" + message += vermsg + "\n" + print(vermsg) + except ValueError: + vermsg = "Error parsing OpenFOAM version string " + foam_ver + message += vermsg + "\n" + print(vermsg) + # Check for wmake + if getFoamRuntime() != "MinGW" and getFoamRuntime() != "PosixDocker": + try: + runFoamCommand("wmake -help") + except subprocess.CalledProcessError: + wmakemsg = "OpenFOAM installation does not include 'wmake'. " + \ + "Installation of cfMesh and HiSA will not be possible." + message += wmakemsg + "\n" + print(wmakemsg) + + # Check for cfMesh + try: + cfmesh_ver = runFoamCommand("cartesianMesh -version")[0] + cfmesh_ver = cfmesh_ver.rstrip().split()[-1] + cfmesh_ver = cfmesh_ver.split('.') + if (not cfmesh_ver or len(cfmesh_ver) != 2 or + int(cfmesh_ver[0]) < CF_MAJOR_VER_REQUIRED or + (int(cfmesh_ver[0]) == CF_MAJOR_VER_REQUIRED and + int(cfmesh_ver[1]) < CF_MINOR_VER_REQUIRED)): + vermsg = "cfMesh-CfdOF version {}.{} required".format(CF_MAJOR_VER_REQUIRED, + CF_MINOR_VER_REQUIRED) + message += vermsg + "\n" + print(vermsg) + except subprocess.CalledProcessError: + cfmesh_msg = "cfMesh (CfdOF version) not found" + message += cfmesh_msg + '\n' + print(cfmesh_msg) + + # Check for HiSA + try: + hisa_ver = runFoamCommand("hisa -version")[0] + hisa_ver = hisa_ver.rstrip().split()[-1] + hisa_ver = hisa_ver.split('.') + if (not hisa_ver or len(hisa_ver) != 3 or + int(hisa_ver[0]) < HISA_MAJOR_VER_REQUIRED or + (int(hisa_ver[0]) == HISA_MAJOR_VER_REQUIRED and + (int(hisa_ver[1]) < HISA_MINOR_VER_REQUIRED or + (int(hisa_ver[1]) == HISA_MINOR_VER_REQUIRED and + int(hisa_ver[2]) < HISA_PATCH_VER_REQUIRED)))): + vermsg = "HiSA version {}.{}.{} required".format(HISA_MAJOR_VER_REQUIRED, + HISA_MINOR_VER_REQUIRED, + HISA_PATCH_VER_REQUIRED) + message += vermsg + "\n" + print(vermsg) + except subprocess.CalledProcessError: + hisa_msg = "HiSA not found" + message += hisa_msg + '\n' + print(hisa_msg) + + # Check for paraview + print("Checking for paraview:") + paraview_cmd = getParaviewExecutable() + failed = False + if not paraview_cmd: + paraview_cmd = 'paraview' + # If not found, try to run from the OpenFOAM environment, in case a bundled version is + # available from there + try: + runFoamCommand('which paraview') + except subprocess.CalledProcessError: + failed = True + if failed or not os.path.exists(paraview_cmd): + pv_msg = "Paraview executable '" + paraview_cmd + "' not found." + message += pv_msg + '\n' + print(pv_msg) + else: + pv_msg = "Paraview executable: {}".format(paraview_cmd) + message += pv_msg + '\n' + print(pv_msg) + + # Check for paraview python support + if not failed: + failed = False + paraview_cmd = getParaviewExecutable() + if not paraview_cmd: + pvpython_cmd = 'pvpython' + # If not found, try to run from the OpenFOAM environment, in case a bundled version is + # available from there + try: + runFoamCommand('which pvpython') + except subprocess.CalledProcessError: + failed = True + else: + if platform.system() == 'Windows': + pvpython_cmd = paraview_cmd.rstrip('paraview.exe')+'pvpython.exe' + else: + pvpython_cmd = paraview_cmd.rstrip('paraview')+'pvpython' + if failed or not os.path.exists(pvpython_cmd): + pv_msg = "Python support in paraview not found. Please install paraview python packages." + message += pv_msg + '\n' + print(pv_msg) + + print("Checking Plot module:") + + try: + import matplotlib + except ImportError: + matplot_msg = "Could not load matplotlib package (required by Plot module)" + message += matplot_msg + '\n' + print(matplot_msg) + + plot_ok = False + if major_ver > 0 or minor_ver >= 20: + try: + from FreeCAD.Plot import Plot # Built-in plot module + plot_ok = True + except ImportError: + plot_msg = "Could not load Plot module\nAttempting to use Plot workbench instead" + message += plot_msg + "\n" + print(plot_msg) + if not plot_ok: + try: + from CfdOF.compat import Plot # Plot workbench + except ImportError: + plot_msg = "Could not load legacy Plot module" + message += plot_msg + '\n' + print(plot_msg) + + print("Checking for gmsh:") + # check that gmsh version 2.13 or greater is installed + gmshversion = "" + gmsh_exe = getGmshExecutable() + if gmsh_exe is None: + gmsh_msg = "gmsh not found (optional)" + message += gmsh_msg + '\n' + print(gmsh_msg) + else: + gmsh_msg = "gmsh executable: " + gmsh_exe + message += gmsh_msg + '\n' + print(gmsh_msg) + try: + # Needs to be runnable from OpenFOAM environment + gmshversion = runFoamCommand("'" + gmsh_exe + "'" + " -version")[2] + except (OSError, subprocess.CalledProcessError): + gmsh_msg = "gmsh could not be run from OpenFOAM environment" + message += gmsh_msg + '\n' + print(gmsh_msg) + if len(gmshversion) > 1: + # Only the last line contains gmsh version number + gmshversion = gmshversion.rstrip().split() + gmshversion = gmshversion[-1] + versionlist = gmshversion.split(".") + if int(versionlist[0]) < 2 or (int(versionlist[0]) == 2 and int(versionlist[1]) < 13): + gmsh_ver_msg = "gmsh version is older than minimum required (2.13)" + message += gmsh_ver_msg + '\n' + print(gmsh_ver_msg) + + print("Completed CFD dependency check") + return message + +#****************************************************************************** +# Done: TODO: right now this routine returns the error code of the outermost process, ie the one called with cmd parameter. +# So if you call ssh, for example, and it runs successfully, it will return no error, no matter what happens to the command +# that was used in ssh. This should be changed. If one gets an error message via stderr, this routine should +# return an error code, regardless of how ssh itself ran. +# +# TODO: add a timer so that the process times out if it doesn't create any output or do anything. +# If ssh credentials aren't set up properly on the remote host, it will ask for a password and not echo that to stdout or stderr. +# Thus the process will be stuck. +# +# This routine is not needed. runFoamCommand does the same thing but is integrated into CfdTools already. +# The only routines that use runCommand are ping and ssh test in RemotePreferences.py. Those routines should be +# rewritten with runFoamCommand and put in CfdTools. +# +# When that happens, this routine can be deleted. + +def runCommand(cmd, args=[]): + + process = QProcess() + returnValue = 0 + + def handleStdout(): + #print("In Stdout handler") + data = process.readAllStandardOutput() + message = bytes(data).decode("utf8") + print(message) + + def handleStderr(): + nonlocal returnValue + #print("In Stderr handler") + data = process.readAllStandardError() + message = bytes(data).decode("utf8") + print("Error:" + message) + returnValue = -1 + + def handleFinished(self, exitCode): + nonlocal returnValue + print("Command done.") + if ((exitCode == QProcess.NormalExit) and (returnValue == 0)): + returnValue = 0 + else: + returnValue = -1 + + # connect the events to handlers + process.readyReadStandardOutput.connect(handleStdout) + process.readyReadStandardError.connect(handleStderr) + process.finished.connect(handleFinished) + + # debug + message = "Remotely executing: " + cmd + for arg in args: + message = message + " " + arg + print(message) + + # start the command process + process.start(cmd, args) + + # wait for the process to get started + process.waitForStarted() + + # process application events while waiting for it to finish + while(process.state() == QProcess.Running): + QApplication.processEvents() + return(returnValue) + + +def getParaviewExecutable(): + # If path of paraview executable specified, use that + paraview_cmd = getParaviewPath() + if not paraview_cmd: + # If using blueCFD, use paraview supplied + if getFoamRuntime() == 'BlueCFD': + paraview_cmd = '{}\\AddOns\\ParaView\\bin\\paraview.exe'.format(getFoamDir()) + elif getFoamRuntime() == 'BlueCFD2': + paraview_cmd = '{}\\..\\AddOns\\ParaView\\bin\\paraview.exe'.format(getFoamDir()) + else: + # Check the defaults + paraview_cmd = findInDefaultPaths(PARAVIEW_PATH_DEFAULTS) + if not paraview_cmd: + # Otherwise, see if the command 'paraview' is in the path. + paraview_cmd = shutil.which('paraview') + return paraview_cmd + + +def getGmshExecutable(): + # If path of gmsh executable specified, use that + gmsh_cmd = getGmshPath() + if not gmsh_cmd: + # On Windows, use gmsh supplied + if platform.system() == "Windows": + # Use forward slashes to avoid escaping problems + gmsh_cmd = '/'.join([FreeCAD.getHomePath().rstrip('/'), 'bin', 'gmsh.exe']) + if not gmsh_cmd: + # Otherwise, see if the command 'gmsh' is in the path. + gmsh_cmd = shutil.which("gmsh") + if getFoamRuntime() == "PosixDocker": + gmsh_cmd='gmsh' + return gmsh_cmd + +def getRemoteGmshExecutable(): + # If path of gmsh executable specified, use that + gmsh_cmd = getGmshPath() + if not gmsh_cmd: + # On Windows, use gmsh supplied + if platform.system() == "Windows": + # Use forward slashes to avoid escaping problems + gmsh_cmd = '/'.join([FreeCAD.getHomePath().rstrip('/'), 'bin', 'gmsh.exe']) + if not gmsh_cmd: + # Otherwise, see if the command 'gmsh' is in the path. + gmsh_cmd = shutil.which("gmsh") + if getFoamRuntime() == "PosixDocker": + gmsh_cmd='gmsh' + return gmsh_cmd + + + +def startParaview(case_path, script_name, console_message_fn): + proc = QtCore.QProcess() + paraview_cmd = getParaviewExecutable() + arg = '--script={}'.format(script_name) + + if not paraview_cmd: + # If not found, try to run from the OpenFOAM environment, in case a bundled version is available from there + paraview_cmd = "$(which paraview)" # 'which' required due to mingw weirdness(?) on Windows + try: + cmds = [paraview_cmd, arg] + cmd = ' '.join(cmds) + console_message_fn("Running " + cmd) + args = makeRunCommand(cmd, case_path) + paraview_cmd = args[0] + args = args[1:] if len(args) > 1 else [] + proc.setProgram(paraview_cmd) + proc.setArguments([arg]) + proc.setProcessEnvironment(getRunEnvironment()) + success = proc.startDetached() + if not success: + raise Exception("Unable to start command " + cmd) + console_message_fn("Paraview started") + except QtCore.QProcess.ProcessError: + console_message_fn("Error starting paraview") + else: + console_message_fn("Running " + paraview_cmd + " " + arg) + proc.setProgram(paraview_cmd) + proc.setArguments([arg]) + proc.setWorkingDirectory(case_path) + env = QtCore.QProcessEnvironment.systemEnvironment() + removeAppimageEnvironment(env) + proc.setProcessEnvironment(env) + success = proc.startDetached() + if success: + console_message_fn("Paraview started") + else: + console_message_fn("Error starting paraview") + return success + + +def startGmsh(working_dir, args, console_message_fn, stdout_fn=None, stderr_fn=None): + proc = CfdConsoleProcess(stdout_hook=stdout_fn, stderr_hook=stderr_fn) + gmsh_cmd = getGmshExecutable() + + if not gmsh_cmd: + console_message_fn("GMSH not found") + else: + console_message_fn("Running " + gmsh_cmd + " " + ' '.join(args)) + + proc.start([gmsh_cmd] + args, working_dir=working_dir) + if proc.waitForStarted(): + console_message_fn("GMSH started") + else: + console_message_fn("Error starting GMSH") + return proc + + +def floatEqual(a, b): + """ + Test whether a and b are equal within an absolute and relative tolerance + """ + reltol = 10*sys.float_info.epsilon + abstol = 1e-12 # Seems to be necessary on file read/write + return abs(a-b) < abstol or abs(a - b) <= reltol*max(abs(a), abs(b)) + + +def isSameGeometry(shape1, shape2): + """ + Copy of FemMeshTools.is_same_geometry, with fixes + """ + # Check Area, CenterOfMass because non-planar shapes might not have more than one vertex defined + same_Vertexes = 0 + # Bugfix: below was 1 - did not work for non-planar shapes + if len(shape1.Vertexes) == len(shape2.Vertexes) and len(shape1.Vertexes) > 0: + # compare CenterOfMass + # Bugfix: Precision seems to be lost on load/save + if hasattr(shape1, "CenterOfMass") and hasattr(shape2, "CenterOfMass"): + if not floatEqual(shape1.CenterOfMass[0], shape2.CenterOfMass[0]) or \ + not floatEqual(shape1.CenterOfMass[1], shape2.CenterOfMass[1]) or \ + not floatEqual(shape1.CenterOfMass[2], shape2.CenterOfMass[2]): + return False + if hasattr(shape1, "Area") and hasattr(shape2, "Area"): + if not floatEqual(shape1.Area, shape2.Area): + return False + # compare the Vertices + for vs1 in shape1.Vertexes: + for vs2 in shape2.Vertexes: + if floatEqual(vs1.X, vs2.X) and floatEqual(vs1.Y, vs2.Y) and floatEqual(vs1.Z, vs2.Z): + same_Vertexes += 1 + # Bugfix: was 'continue' - caused false-negative with repeated vertices + break + if same_Vertexes == len(shape1.Vertexes): + return True + else: + return False + + +def findElementInShape(a_shape, an_element): + """ + Copy of FemMeshTools.find_element_in_shape, but calling isSameGeometry + """ + # import Part + ele_st = an_element.ShapeType + if ele_st == 'Solid' or ele_st == 'CompSolid': + for index, solid in enumerate(a_shape.Solids): + # print(is_same_geometry(solid, anElement)) + if isSameGeometry(solid, an_element): + # print(index) + # Part.show(aShape.Solids[index]) + ele = ele_st + str(index + 1) + return ele + FreeCAD.Console.PrintError('Solid ' + str(an_element) + ' not found in: ' + str(a_shape) + '\n') + if ele_st == 'Solid' and a_shape.ShapeType == 'Solid': + print('We have been searching for a Solid in a Solid and we have not found it. In most cases this should be searching for a Solid inside a CompSolid. Check the ShapeType of your Part to mesh.') + # Part.show(anElement) + # Part.show(aShape) + elif ele_st == 'Face' or ele_st == 'Shell': + for index, face in enumerate(a_shape.Faces): + # print(is_same_geometry(face, anElement)) + if isSameGeometry(face, an_element): + # print(index) + # Part.show(aShape.Faces[index]) + ele = ele_st + str(index + 1) + return ele + elif ele_st == 'Edge' or ele_st == 'Wire': + for index, edge in enumerate(a_shape.Edges): + # print(is_same_geometry(edge, anElement)) + if isSameGeometry(edge, an_element): + # print(index) + # Part.show(aShape.Edges[index]) + ele = ele_st + str(index + 1) + return ele + elif ele_st == 'Vertex': + for index, vertex in enumerate(a_shape.Vertexes): + # print(is_same_geometry(vertex, anElement)) + if isSameGeometry(vertex, an_element): + # print(index) + # Part.show(aShape.Vertexes[index]) + ele = ele_st + str(index + 1) + return ele + elif ele_st == 'Compound': + FreeCAD.Console.PrintError('Compound is not supported.\n') + + +def matchFaces(faces1, faces2): + """ + This function does a geometric matching of face lists much faster than doing face-by-face search + :param faces1: List of tuples - first item is face object, second is any user data + :param faces2: List of tuples - first item is face object, second is any user data + :return: A list of (data1, data2) containing the user data for any/all matching faces + Note that faces1 and faces2 are sorted in place and can be re-used for faster subsequent searches + """ + + if sys.version_info >= (3,): # Python 3 + + def compKeyFn(key): + class K(object): + def __init__(self, val, *args): + self.val = key(val) + + def __eq__(self, other): + return floatEqual(self.val, other.val) + + def __ne__(self, other): + return not floatEqual(self.val, other.val) + + def __lt__(self, other): + return self.val < other.val and not floatEqual(self.val, other.val) + + def __gt__(self, other): + return self.val > other.val and not floatEqual(self.val, other.val) + + def __le__(self, other): + return self.val < other.val or floatEqual(self.val, other.val) + + def __ge__(self, other): + return self.val > other.val or floatEqual(self.val, other.val) + + return K + + # Sort face list by first vertex, x then y then z in case all in plane + faces1.sort(key=compKeyFn(lambda bf: bf[0].Vertexes[0].Point.z)) + faces1.sort(key=compKeyFn(lambda bf: bf[0].Vertexes[0].Point.y)) + faces1.sort(key=compKeyFn(lambda bf: bf[0].Vertexes[0].Point.x)) + + # Same on other face list + faces2.sort(key=compKeyFn(lambda mf: mf[0].Vertexes[0].Point.z)) + faces2.sort(key=compKeyFn(lambda mf: mf[0].Vertexes[0].Point.y)) + faces2.sort(key=compKeyFn(lambda mf: mf[0].Vertexes[0].Point.x)) + + else: # Python 2 + + def compFn(x, y): + if floatEqual(x, y): + return 0 + elif x < y: + return -1 + else: + return 1 + + # Sort face list by first vertex, x then y then z in case all in plane + faces1.sort(cmp=compFn, key=lambda bf: bf[0].Vertexes[0].Point.z) + faces1.sort(cmp=compFn, key=lambda bf: bf[0].Vertexes[0].Point.y) + faces1.sort(cmp=compFn, key=lambda bf: bf[0].Vertexes[0].Point.x) + + # Same on other face list + faces2.sort(cmp=compFn, key=lambda mf: mf[0].Vertexes[0].Point.z) + faces2.sort(cmp=compFn, key=lambda mf: mf[0].Vertexes[0].Point.y) + faces2.sort(cmp=compFn, key=lambda mf: mf[0].Vertexes[0].Point.x) + + # Find faces with matching first vertex + i = 0 + j = 0 + j_match_start = 0 + matching = False + candidate_mesh_faces = [] + while i < len(faces1) and j < len(faces2): + bf = faces1[i][0] + mf = faces2[j][0] + if floatEqual(bf.Vertexes[0].Point.x, mf.Vertexes[0].Point.x): + if floatEqual(bf.Vertexes[0].Point.y, mf.Vertexes[0].Point.y): + if floatEqual(bf.Vertexes[0].Point.z, mf.Vertexes[0].Point.z): + candidate_mesh_faces.append((i, j)) + cmp = 0 + else: + cmp = (-1 if bf.Vertexes[0].Point.z < mf.Vertexes[0].Point.z else 1) + else: + cmp = (-1 if bf.Vertexes[0].Point.y < mf.Vertexes[0].Point.y else 1) + else: + cmp = (-1 if bf.Vertexes[0].Point.x < mf.Vertexes[0].Point.x else 1) + if cmp == 0: + if not matching: + j_match_start = j + j += 1 + matching = True + if j == len(faces2): + i += 1 + j = j_match_start + matching = False + elif cmp < 0: + i += 1 + if matching: + j = j_match_start + matching = False + elif cmp > 0: + j += 1 + matching = False + + # Do comprehensive matching, and reallocate to original index + successful_candidates = [] + for k in range(len(candidate_mesh_faces)): + i, j = candidate_mesh_faces[k] + if isSameGeometry(faces1[i][0], faces2[j][0]): + successful_candidates.append((faces1[i][1], faces2[j][1])) + + return successful_candidates + + +def makeShapeFromReferences(refs, raise_error=True): + face_list = [] + for ref in refs: + shapes = resolveReference(ref, raise_error) + if len(shapes): + face_list += [s[0] for s in shapes] + if len(face_list) > 0: + shape = Part.makeCompound(face_list) + return shape + else: + return None + + +def resolveReference(r, raise_error=True): + obj = r[0] + if not r[1] or r[1] == ('',): + return [(obj.Shape, (r[0], None))] + f = [] + for rr in r[1]: + try: + if rr.startswith('Solid'): # getElement doesn't work with solids for some reason + f += [(obj.Shape.Solids[int(rr.lstrip('Solid')) - 1], (r[0], rr))] + else: + ff = obj.Shape.getElement(rr) + if ff is None: + if raise_error: + raise RuntimeError("Face '{}:{}' was not found - geometry may have changed".format(r[0].Name, rr)) + else: + f += [(ff, (r[0], rr))] + except Part.OCCError: + if raise_error: + raise RuntimeError("Face '{}:{}' was not found - geometry may have changed".format(r[0].Name, r[1])) + return f + + +def setActiveAnalysis(analysis): + from CfdOF.CfdAnalysis import CfdAnalysis + for obj in FreeCAD.ActiveDocument.Objects: + if hasattr(obj, 'Proxy') and isinstance(obj.Proxy, CfdAnalysis): + obj.IsActiveAnalysis = False + + analysis.IsActiveAnalysis = True + + +def getActiveAnalysis(): + from CfdOF.CfdAnalysis import CfdAnalysis + for obj in FreeCAD.ActiveDocument.Objects: + if hasattr(obj, 'Proxy') and isinstance(obj.Proxy, CfdAnalysis): + if obj.IsActiveAnalysis: + return obj + return None + + +def addObjectProperty(obj, prop, init_val, type, *args): + """ + Call addProperty on the object if it does not yet exist + """ + added = False + if prop not in obj.PropertiesList: + added = obj.addProperty(type, prop, *args) + if type == 'App::PropertyQuantity': + # Set the unit so that the quantity will be accepted + # Has to be repeated on load as unit gets lost + setattr(obj, prop, Units.Unit(init_val)) + if added: + setattr(obj, prop, init_val) + elif type == 'App::PropertyEnumeration': + # For enumeration, re-assign the list of allowed values anyway in case some were added + # Make sure the currently set value is unaffected by this + curr_item = getattr(obj, prop) + setattr(obj, prop, init_val) + setattr(obj, prop, curr_item) + return added + + +def relLenToRefinementLevel(rel_len): + return math.ceil(math.log(1.0/rel_len)/math.log(2)) + + +def importMaterials(): + materials = {} + material_name_path_list = [] + + # Store the defaults inside the module directory rather than the resource dir + # system_mat_dir = FreeCAD.getResourceDir() + "/Mod/Material/FluidMaterialProperties" + system_mat_dir = os.path.join(getModulePath(), "Data", "CfdFluidMaterialProperties") + material_name_path_list = material_name_path_list + addMatDir(system_mat_dir, materials) + return materials, material_name_path_list + + +def addMatDir(mat_dir, materials): + import importFCMat + mat_file_extension = ".FCMat" + ext_len = len(mat_file_extension) + dir_path_list = glob.glob(mat_dir + '/*' + mat_file_extension) + material_name_path_list = [] + for a_path in dir_path_list: + material_name = os.path.basename(a_path[:-ext_len]) + materials[a_path] = importFCMat.read(a_path) + material_name_path_list.append([material_name, a_path]) + material_name_path_list.sort() + + return material_name_path_list + + +def propsToDict(obj): + """ + Convert an object's properties to dictionary entries, converting any PropertyQuantity to float in SI units + """ + + d = {} + for k in obj.PropertiesList: + if obj.getTypeIdOfProperty(k) in QUANTITY_PROPERTIES: + q = Units.Quantity(getattr(obj, k)) + # q.Value is in FreeCAD internal units, which is same as SI except for mm instead of m + d[k] = q.Value/1000**q.Unit.Signature[0] + else: + d[k] = getattr(obj, k) + return d + + +def openFileManager(case_path): + case_path = os.path.abspath(case_path) + if platform.system() == 'MacOS': + subprocess.Popen(['open', '--', case_path]) + elif platform.system() == 'Linux': + subprocess.Popen(['xdg-open', case_path]) + elif platform.system() == 'Windows': + subprocess.Popen(['explorer', case_path]) + + +def writePatchToStl(solid_name, face_mesh, fid, scale=1): + fid.write("solid {}\n".format(solid_name)) + for face in face_mesh.Facets: + n = face.Normal + fid.write(" facet normal {} {} {}\n".format(n[0], n[1], n[2])) + fid.write(" outer loop\n") + for i in range(3): + p = [i * scale for i in face.Points[i]] + fid.write(" vertex {} {} {}\n".format(p[0], p[1], p[2])) + fid.write(" endloop\n") + fid.write(" endfacet\n") + fid.write("endsolid {}\n".format(solid_name)) + + +def enableLayoutRows(layout, selected_rows): + if isinstance(layout, QFormLayout): + for rowi in range(layout.count()): + for role in [QFormLayout.LabelRole, QFormLayout.FieldRole, QFormLayout.SpanningRole]: + item = layout.itemAt(rowi, role) + if item: + if isinstance(item, QtGui.QWidgetItem): + item.widget().setVisible(selected_rows is None or rowi in selected_rows) + elif isinstance(layout, QGridLayout): + for rowi in range(layout.rowCount()): + for coli in range(layout.columnCount()): + item = layout.itemAtPosition(rowi, coli) + if item: + if isinstance(item, QtGui.QWidgetItem): + item.widget().setVisible(selected_rows is None or rowi in selected_rows) + else: + for rowi in range(layout.count()): + item = layout.itemAt(rowi) + if item: + if isinstance(item, QtGui.QWidgetItem): + item.widget().setVisible(selected_rows is None or rowi in selected_rows) + + +def clearCase(case_path, backup_path=None): + """ + Remove and recreate contents of the case directory, optionally backing up + Does not remove the directory entry itself as this requires paraview to be reloaded + """ + if backup_path: + os.makedirs(backup_path) #mkdir -p + if os.path.isdir(case_path): + for entry in os.scandir(case_path): + if backup_path: + dest = os.path.join(backup_path, entry.name) + shutil.move(entry.path, dest) + else: + if entry.is_dir(): + shutil.rmtree(entry.path) + else: + os.remove(entry.path) + else: + os.makedirs(case_path) # mkdir -p + + +def executeMacro(macro_name): + macro_contents = "import FreeCAD\nimport FreeCADGui\nimport FreeCAD as App\nimport FreeCADGui as Gui\n" + macro_contents += open(macro_name).read() + exec(compile(macro_contents, macro_name, 'exec'), {'__file__': macro_name}) + + +class CfdSynchronousFoamProcess: + def __init__(self): + self.process = CfdConsoleProcess(stdout_hook=self.readOutput, stderr_hook=self.readError) + self.output = "" + self.outputErr = "" + self.outputAll = "" + + def run(self, cmdline, case=None): + if getFoamRuntime() == "PosixDocker" and ' pull ' not in cmdline: + if startDocker(): + return 1 + print("Running ", cmdline) + + mycmdline = makeRunCommand(cmdline,case) + print("cmdline in CfdSynFoamProcess:") + print(mycmdline) + + self.process.start(makeRunCommand(cmdline, case), env_vars=getRunEnvironment()) + if not self.process.waitForFinished(): + raise Exception("Unable to run command " + cmdline) + return self.process.exitCode() + + def readOutput(self, output): + self.output += output + self.outputAll += output + + def readError(self, output): + self.outputErr += output + self.outputAll += output + + + +# Only one container is needed. Start one for the CfdOF workbench as required +class DockerContainer: + container_id = None + usedocker = False + output_path_used = None + docker_cmd = None + + def __init__(self): + self.image_name = None + import shutil + + if shutil.which('podman') is not None: + self.docker_cmd = shutil.which('podman') + elif shutil.which('docker') is not None: + self.docker_cmd = shutil.which('docker') + else: + self.docker_cmd = None + + if self.docker_cmd is not None: + self.docker_cmd = self.docker_cmd.split(os.path.sep)[-1] + + def start_container(self): + prefs = getPreferencesLocation() + self.image_name = FreeCAD.ParamGet(prefs).GetString("DockerURL", "") + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + + if not output_path: + output_path = tempfile.gettempdir() + output_path = os.path.normpath(output_path) + + if not os.path.isdir(output_path): + print("Default output directory not found") + return 1 + + if DockerContainer.container_id != None: + print("Attempting to start container but id already set") + + container_ls = self.query_docker_container_ls() + if container_ls != None: + print("Docker container {} running but not started by CfdOF - it may not be configured correctly - stopping container.".format(container_ls)) + self.stop_container(alien = True) + + if self.image_name == "": + print("Docker image name not set") + return 1 + + if self.docker_cmd == None: + print("Need to install either podman or docker") + return 1 + + if platform.system() == 'Windows': + out_d = output_path.split(os.sep) + if len(out_d)>2 and out_d[2][:3] == 'wsl': + output_path = '/' + '/'.join(out_d[4:]) + + cmd = "{0} run -t -d -u 1000:1000 -v {1}:/tmp {2}".format(self.docker_cmd, output_path, self.image_name) + + if 'docker' in self.docker_cmd: + cmd = cmd.replace('docker.io/','') + proc = QtCore.QProcess() + proc.start(cmd) + self.getContainerID() + if self.container_id != None: + self.output_path_used = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + return 0 + else: + return 1 + + """ Stop docker container and remove """ + def stop_container(self, alien = False): + if DockerContainer.container_id == None and alien: + DockerContainer.container_id = self.query_docker_container_ls() + if DockerContainer.container_id != None: + print("Stopping docker container {}".format(DockerContainer.container_id)) + cmd = "{} container rm -f {}".format(self.docker_cmd, DockerContainer.container_id) + proc = QtCore.QProcess() + proc.start(cmd) + if not proc.waitForFinished(): + print("Stop docker container {} failed".format(DockerContainer.container_id)) + else: + while proc.canReadLine(): + line = proc.readLine() + DockerContainer.container_id = None + DockerContainer.output_path_used = None + else: + print("No docker container to stop") + + def getContainerID(self): + import time + timer_counts = 0 + while timer_counts < 10 and DockerContainer.container_id == None: + time.sleep(1) + DockerContainer.container_id = self.query_docker_container_ls() + timer_counts = timer_counts + 1 + + def query_docker_container_ls(self): + cmd = "{} container ls".format(self.docker_cmd) + proc = QtCore.QProcess() + proc.start(cmd) + proc.waitForFinished() + while proc.canReadLine(): + line = proc.readLine() + cnt_lst = str(line.data(), encoding="utf-8").split() + if len(cnt_lst)>0 and cnt_lst[1].replace(':latest','').replace('docker.io/','') == self.image_name.replace(':latest','').replace('docker.io/',''): + return(cnt_lst[0]) + return(None) + diff --git a/TaskPanelCfdMesh.py b/TaskPanelCfdMesh.py new file mode 100644 index 00000000..6df3add3 --- /dev/null +++ b/TaskPanelCfdMesh.py @@ -0,0 +1,710 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2016 - Bernd Hahnebach * +# * Copyright (c) 2017 Alfred Bogaers (CSIR) * +# * Copyright (c) 2017 Johan Heyns (CSIR) * +# * Copyright (c) 2017 Oliver Oxtoby (CSIR) * +# * Copyright (c) 2019-2022 Oliver Oxtoby * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 3 of the * +# * License, or (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with this program. If not, * +# * see . * +# * * +# *************************************************************************** +# +# LinuxGuy123@gmail.com's notes: +# +# +# +# TODOs, in addition to TODOs in the code itself +# +# - check that the appropriate controls are enabled for the host. Most specifically, +# Edit case shouldn't be enabled for remote hosts. Paraview doesn't work on remote hosts either. +# Also check mesh, etc. +# +# - right now there is no way to edit the case on a remote host. This could be enabled by +# copying back the case to the local machine, allowing the user to edit the files in a temp dir +# and then copying them back to the remote host +# +# - the host name was passed into the meshing routines via the global variable. Really the hostname +# should be passed into the meshing routines via the mesh object +# +#- add use filename extension to the output path. For both local and remote useRemoteProcessing +# +#- copy the mesh back to the local computer for Paraview, Load surface mesh and Check Mesh. + + + + +from __future__ import print_function +import FreeCAD +import os +import os.path +from CfdOF.Mesh import CfdMesh +import time +from datetime import timedelta +from CfdOF import CfdTools +from CfdOF.CfdTools import setQuantity, getQuantity, storeIfChanged +from CfdOF.Mesh import CfdMeshTools +from CfdOF.CfdConsoleProcess import CfdConsoleProcess +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore + from PySide import QtCore + from PySide import QtGui + from PySide.QtCore import Qt + from PySide.QtGui import QApplication + + +class TaskPanelCfdMesh: + """ The TaskPanel for editing References property of CfdMesh objects and creation of new CFD mesh """ + def __init__(self, obj): + self.mesh_obj = obj + self.analysis_obj = CfdTools.getParentAnalysisObject(self.mesh_obj) + self.form = FreeCADGui.PySideUic.loadUi(os.path.join(CfdTools.getModulePath(), 'Gui', "TaskPanelCfdMesh.ui")) + + self.console_message_cart = '' + self.error_message = '' + self.mesh_obj.Proxy.cart_mesh = CfdMeshTools.CfdMeshTools(self.mesh_obj) + self.paraviewScriptName = "" + + self.mesh_obj.Proxy.mesh_process = CfdConsoleProcess(finished_hook=self.meshFinished, + stdout_hook=self.gotOutputLines, + stderr_hook=self.gotErrorLines) + + #set the prefs and host prefs locations + self.prefs_location = CfdTools.getPreferencesLocation() + self.host_prefs_location = self.prefs_location + "/Hosts" + self.useRemoteProcessing = FreeCAD.ParamGet(self.prefs_location).GetBool('UseRemoteProcessing', 0) + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + + self.profile_name = "" + self.hostname = "" + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.gmsh_path = "" + self.add_filename_to_output = False + + #add a local host to cb_profile + self.form.cb_profile.addItem("local") + + # if using remote processing, add the host profiles as well + if self.useRemoteProcessing: + self.loadProfileNames() + else: + #disable cb_profile so that users aren't trying to change the host + self.form.cb_profile.setEnabled(False) + + # load the local profile + self.loadProfile("local") + + self.Timer = QtCore.QTimer() + self.Timer.setInterval(1000) + self.Timer.timeout.connect(self.update_timer_text) + + # set up the profiles combo box connection + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) + + self.form.cb_utility.activated.connect(self.choose_utility) + + self.form.pb_write_mesh.clicked.connect(self.writeMesh) + + self.form.pb_edit_mesh.clicked.connect(self.editMesh) + + self.form.pb_run_mesh.clicked.connect(self.runMesh) + + self.form.pb_stop_mesh.clicked.connect(self.killMeshProcess) + self.form.pb_paraview.clicked.connect(self.openParaview) + self.form.pb_load_mesh.clicked.connect(self.pbLoadMeshClicked) + self.form.pb_clear_mesh.clicked.connect(self.pbClearMeshClicked) + self.form.pb_searchPointInMesh.clicked.connect(self.searchPointInMesh) + self.form.pb_check_mesh.clicked.connect(self.checkMeshClicked) + + self.radioGroup = QtGui.QButtonGroup() + self.radioGroup.addButton(self.form.radio_explicit_edge_detection) + self.radioGroup.addButton(self.form.radio_implicit_edge_detection) + + self.form.snappySpecificProperties.setVisible(False) + self.form.pb_stop_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + + self.form.cb_utility.addItems(CfdMesh.MESHER_DESCRIPTIONS) + + self.form.if_max.setToolTip("Enter 0 to use default value") + self.form.pb_searchPointInMesh.setToolTip("Specify below a point vector inside of the mesh or press 'Search' " + "to try to automatically find a point") + self.form.if_cellsbetweenlevels.setToolTip("Number of cells between each of level of refinement") + self.form.if_edgerefine.setToolTip("Number of refinement levels for all edges") + self.form.radio_explicit_edge_detection.setToolTip("Find surface edges using explicit (eMesh) detection") + self.form.radio_implicit_edge_detection.setToolTip("Find surface edges using implicit detection") + + self.load() + self.updateUI() + + self.Start = time.time() + self.Timer.start() + + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + print("Error: no host profile selected") + return + + # set the vars to the local parameters + if profile_name == "local": + self.hostname = "local" + # the local code doesn't use these vars, so don't set them + # dangerous. + """ + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = "" + self.add_filename_to_output = False + """ + else: + #set the vars to the remote host parameters + # most of these aren't used, at least not in this page + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + + #now set the control values + self.mesh_obj.NumberOfProcesses = self.mesh_processes + self.mesh_obj.NumberOfThreads = self.mesh_threads + + #TODO: fix these, if we need to. + #self.form.le_mesh_processes.setText(str(self.mesh_processes)) + #self.form.le_mesh_threads.setText(str(self.mesh_threads)) + + #self.form.le_hostname.setText(self.hostname) + #self.form.le_username.setText(self.username) + + #self.form.le_foam_processes.setText(str(self.foam_processes)) + #self.form.le_foam_threads.setText(str(self.foam_threads)) + #self.form.le_foam_dir.setText(self.foam_dir) + #self.form.le_output_path.setText(self.output_path) + #self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + + + # this gets called when the user changes the profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + # TODO enable and disable the appropriate controls here + # Remote hosts can't edit the case nor Paraview, check mesh, etc. + # Nor load surface mesh nor clear surface mesh. + + + # test routine to run a mesh without + # a proxy. The real routine is runMesh way below + def runRemoteMesh(self): + # run remote meshing directly, without a proxy + #profile_prefs = CfdTools.getPreferencesLocation() + '/Hosts/' + self.profile_name + remote_user = self.username + remote_hostname = self.hostname + + # create the ssh connection command + ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' ' + + # Get the working directory for the mesh + working_dir = self.output_path + #TODO: add filename to the path if selected + + # create the command to do the actual work + command = 'EOT \n' + command += 'cd ' + working_dir + '/meshCase \n' + command += './Allmesh \n' + command += 'exit \n' + command += 'EOT' + command = ssh_prefix + ' << ' + command + + self.consoleMessage("Starting remote meshing...") + try: + CfdTools.runFoamCommand(command) + print("Remote meshing is complete.") + self.consoleMessage("Remote meshing is complete.") + except Exception as error: + self.consoleMessage("Error meshing on remote host: " + str(error)) + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Close) + # def reject() is called on close button + + def reject(self): + FreeCADGui.ActiveDocument.resetEdit() + return True + + def closed(self): + # We call this from unsetEdit to ensure cleanup + self.store() + self.mesh_obj.Proxy.mesh_process.terminate() + self.mesh_obj.Proxy.mesh_process.waitForFinished() + self.Timer.stop() + FreeCAD.ActiveDocument.recompute() + + def load(self): + """ Fills the widgets """ + setQuantity(self.form.if_max, self.mesh_obj.CharacteristicLengthMax) + point_in_mesh = self.mesh_obj.PointInMesh.copy() + setQuantity(self.form.if_pointInMeshX, point_in_mesh.get('x')) + setQuantity(self.form.if_pointInMeshY, point_in_mesh.get('y')) + setQuantity(self.form.if_pointInMeshZ, point_in_mesh.get('z')) + + self.form.if_cellsbetweenlevels.setValue(self.mesh_obj.CellsBetweenLevels) + self.form.if_edgerefine.setValue(self.mesh_obj.EdgeRefinement) + self.form.radio_implicit_edge_detection.setChecked(self.mesh_obj.ImplicitEdgeDetection) + self.form.radio_explicit_edge_detection.setChecked(not self.mesh_obj.ImplicitEdgeDetection) + + index_utility = CfdTools.indexOrDefault(list(zip( + CfdMesh.MESHERS, CfdMesh.DIMENSION, CfdMesh.DUAL_CONVERSION)), + (self.mesh_obj.MeshUtility, self.mesh_obj.ElementDimension, self.mesh_obj.ConvertToDualMesh), 0) + self.form.cb_utility.setCurrentIndex(index_utility) + + def updateUI(self): + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir + self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) + self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) + self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + + utility = CfdMesh.MESHERS[self.form.cb_utility.currentIndex()] + if utility == "snappyHexMesh": + self.form.snappySpecificProperties.setVisible(True) + else: + self.form.snappySpecificProperties.setVisible(False) + + def store(self): + mesher_idx = self.form.cb_utility.currentIndex() + storeIfChanged(self.mesh_obj, 'CharacteristicLengthMax', getQuantity(self.form.if_max)) + storeIfChanged(self.mesh_obj, 'MeshUtility', CfdMesh.MESHERS[mesher_idx]) + storeIfChanged(self.mesh_obj, 'ElementDimension', CfdMesh.DIMENSION[mesher_idx]) + storeIfChanged(self.mesh_obj, 'CellsBetweenLevels', self.form.if_cellsbetweenlevels.value()) + storeIfChanged(self.mesh_obj, 'EdgeRefinement', self.form.if_edgerefine.value()) + storeIfChanged(self.mesh_obj, 'ConvertToDualMesh', CfdMesh.DUAL_CONVERSION[mesher_idx]) + storeIfChanged(self.mesh_obj, 'ImplicitEdgeDetection', self.form.radio_implicit_edge_detection.isChecked()) + + point_in_mesh = {'x': getQuantity(self.form.if_pointInMeshX), + 'y': getQuantity(self.form.if_pointInMeshY), + 'z': getQuantity(self.form.if_pointInMeshZ)} + + if self.mesh_obj.MeshUtility == 'snappyHexMesh': + storeIfChanged(self.mesh_obj, 'PointInMesh', point_in_mesh) + + self.mesh_obj.Proxy.cart_mesh = CfdMeshTools.CfdMeshTools(self.mesh_obj) + + def consoleMessage(self, message="", colour_type=None, timed=True): + if timed: + self.console_message_cart += \ + '{:4.1f}: '.format(CfdTools.getColour('Logging'), time.time() - self.Start) + if colour_type: + self.console_message_cart += \ + '{}
'.format(CfdTools.getColour(colour_type), message) + else: + self.console_message_cart += message + '
' + self.form.te_output.setText(self.console_message_cart) + self.form.te_output.moveCursor(QtGui.QTextCursor.End) + if FreeCAD.GuiUp: + FreeCAD.Gui.updateGui() + + def update_timer_text(self): + if self.mesh_obj.Proxy.mesh_process.state() == QtCore.QProcess.ProcessState.Running: + self.form.l_time.setText('Time: ' + CfdTools.formatTimer(time.time() - self.Start)) + + def choose_utility(self, index): + if index < 0: + return + utility = CfdMesh.MESHERS[self.form.cb_utility.currentIndex()] + if utility == "snappyHexMesh": + self.form.snappySpecificProperties.setVisible(True) + else: + self.form.snappySpecificProperties.setVisible(False) + + def writeMesh(self): + import importlib + importlib.reload(CfdMeshTools) + self.console_message_cart = '' + self.Start = time.time() + # Re-initialise CfdMeshTools with new parameters + self.store() + + #get the host name we are writing the mesh case for + host_profile = self.profile_name + print ("Writing mesh for host profile " + host_profile + ".") + + FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") + FreeCADGui.doCommand("from CfdOF import CfdTools") + FreeCADGui.doCommand("cart_mesh = " + "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") + FreeCADGui.doCommand("FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy.cart_mesh = cart_mesh") + cart_mesh = self.mesh_obj.Proxy.cart_mesh + cart_mesh.progressCallback = self.progressCallback + + # Start writing the mesh files + if host_profile == "local": + self.consoleMessage("Preparing local mesh ...") + else: + self.consoleMessage("Preparing remote mesh for " + host_profile + "...") + try: + QApplication.setOverrideCursor(Qt.WaitCursor) + setQuantity(self.form.if_max, str(cart_mesh.getClmax())) + # Re-update the data in case ClMax was auto-set to avoid spurious update detection on next write + self.store() + print('Part to mesh:\n Name: ' + + cart_mesh.part_obj.Name + ', Label: ' + + cart_mesh.part_obj.Label + ', ShapeType: ' + + cart_mesh.part_obj.Shape.ShapeType) + print(' CharacteristicLengthMax: ' + str(cart_mesh.clmax)) + + if host_profile == "local": + FreeCADGui.doCommand("cart_mesh.writeMesh('local')") + else: + FreeCADGui.doCommand("cart_mesh.writeMesh('"+ host_profile +"')") + + except Exception as ex: + self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') + raise + else: + self.analysis_obj.NeedsMeshRerun = True + finally: + QApplication.restoreOverrideCursor() + + # Update the UI + self.updateUI() + + + def progressCallback(self, message): + self.consoleMessage(message) + + def checkMeshClicked(self): + if CfdTools.getFoamRuntime() == "PosixDocker": + CfdTools.startDocker() + self.Start = time.time() + try: + QApplication.setOverrideCursor(Qt.WaitCursor) + FreeCADGui.doCommand("from CfdOF import CfdTools") + FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") + FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") + FreeCADGui.doCommand("cart_mesh = " + "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") + FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") + FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") + FreeCADGui.doCommand("cart_mesh.error = False") + FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('checkMesh -meshQuality', cart_mesh.meshCaseDir)") + FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(cmd, env_vars=env_vars)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(cmd, env_vars=env_vars)") + if self.mesh_obj.Proxy.mesh_process.waitForStarted(): + self.form.pb_check_mesh.setEnabled(False) # Prevent user running a second instance + self.form.pb_run_mesh.setEnabled(False) + self.form.pb_write_mesh.setEnabled(False) + #self.form.pb_write_remote_mesh.setEnabled(False) + self.form.pb_stop_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + self.consoleMessage("Mesh check started ...") + else: + self.consoleMessage("Error starting mesh check process", 'Error') + self.mesh_obj.Proxy.cart_mesh.error = True + + except Exception as ex: + self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') + finally: + QApplication.restoreOverrideCursor() + + + def editMesh(self): + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir + self.consoleMessage("Please edit the case input files externally at: {}\n".format(case_path)) + CfdTools.openFileManager(case_path) + + + #TODO: won't run remotely with a proxy yet. Fix this. + # Presently running without a proxy in runRemoteMesh way above. + def runMesh(self): + if CfdTools.getFoamRuntime() == "PosixDocker": + CfdTools.startDocker() + + self.Start = time.time() + + # Check for changes that require mesh re-write + self.store() + if self.analysis_obj.NeedsMeshRewrite: + if FreeCAD.GuiUp: + if QtGui.QMessageBox.question( + None, + "CfdOF Workbench", + "The case setup for the mesher may need to be re-written based on changes you have made to the " + "model.\n\nWrite mesh case first?", defaultButton=QtGui.QMessageBox.Yes + ) == QtGui.QMessageBox.Yes: + self.Start = time.time() + self.writeMesh() + else: + self.Start = time.time() + + try: + QApplication.setOverrideCursor(Qt.WaitCursor) + + self.consoleMessage("Initializing {} ...".format(self.mesh_obj.MeshUtility)) + FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") + FreeCADGui.doCommand("from CfdOF import CfdTools") + FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") + FreeCADGui.doCommand("from FreeCAD import ParamGet") + + FreeCADGui.doCommand("cart_mesh = " + + "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") + + FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") + FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") + FreeCADGui.doCommand("cart_mesh.error = False") + + # run locally + if self.profile_name == "local": + FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False)") + FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") + + FreeCADGui.doCommand("print('cmd:')") + FreeCADGui.doCommand("print(cmd)") + #FreeCADGui.doCommand("print('env_vars:' + env_vars)") + + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(cmd, env_vars=env_vars)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(cmd, env_vars=env_vars)") + + # run on remote host + else: + #self.runRemoteMesh() #For testing the non proxy function above + + # Get the username and hostname for the remote host + prefsCmd = "profile_prefs = CfdTools.getPreferencesLocation() + " + '"/Hosts/' + self.profile_name + '"' + #print("prefsCmd:" + prefsCmd) + FreeCADGui.doCommand(prefsCmd) + FreeCADGui.doCommand("print('profile_prefs:' + profile_prefs)") + + FreeCADGui.doCommand("remote_user = FreeCAD.ParamGet(profile_prefs).GetString('Username', '')") + FreeCADGui.doCommand("remote_hostname =FreeCAD.ParamGet(profile_prefs).GetString('Hostname', '')") + + #FreeCADGui.doCommand("print('username:' + remote_user)") + #FreeCADGui.doCommand("print('hostname:' + remote_hostname)") + + # create the ssh connection command + FreeCADGui.doCommand("ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' '") + + # Get the working directory for the mesh + FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") + #FreeCADGui.doCommand("print('working directory:' + working_dir)") + + + # create the command to do the actual work + FreeCADGui.doCommand("command = 'EOT \\n' \n" + + "command += 'cd ' + working_dir + '/meshCase \\n' \n" + + "command += './Allmesh \\n' \n" + + "command += 'exit \\n' \n" + + "command += 'EOT' \n") + FreeCADGui.doCommand("command = ssh_prefix + ' << ' + command + '\\n'") + + #FreeCADGui.doCommand("print(command)") + + FreeCADGui.doCommand("runCommand = CfdTools.makeRunCommand(command, None)") + + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(runCommand)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(runCommand)") + + time.sleep(2) + if self.mesh_obj.Proxy.mesh_process.waitForStarted(): + # enable/disable the correct buttons + """ + if self.profile_name == "local": + self.form.pb_stop_mesh.setEnabled(True) + + self.form.pb_run_mesh.setEnabled(False) + #self.form.pb_run_remote_mesh.setEnabled(False) + self.form.pb_write_mesh.setEnabled(False) + #self.form.pb_write_remote_mesh.setEnabled(False) + self.form.pb_edit_mesh.setEnabled(False) + #self.form.pb_edit_remote_mesh.setEnabled(False) + + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + else: + if self.useRemoteProcessing: + #self.form.pb_stop_remote_mesh.setEnabled(True) + + self.form.pb_run_mesh.setEnabled(False) + #self.form.pb_run_remote_mesh.setEnabled(False) + self.form.pb_write_mesh.setEnabled(False) + self.form.pb_write_remote_mesh.setEnabled(False) + self.form.pb_edit_mesh.setEnabled(False) + self.form.pb_edit_remote_mesh.setEnabled(False) + + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + """ + self.consoleMessage("Mesher started ...") + else: + self.consoleMessage("Error starting meshing process", 'Error') + self.mesh_obj.Proxy.cart_mesh.error = True + except Exception as ex: + self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') + raise + finally: + QApplication.restoreOverrideCursor() + + + def killMeshProcess(self): + self.consoleMessage("Meshing manually stopped") + self.error_message = 'Meshing interrupted' + self.mesh_obj.Proxy.mesh_process.terminate() + # Note: meshFinished will still be called + + def gotOutputLines(self, lines): + pass + + def gotErrorLines(self, lines): + print_err = self.mesh_obj.Proxy.mesh_process.processErrorOutput(lines) + if print_err is not None: + self.consoleMessage(print_err, 'Error') + + def meshFinished(self, exit_code): + if exit_code == 0: + self.consoleMessage('Meshing completed') + self.analysis_obj.NeedsMeshRerun = False + self.form.pb_run_mesh.setEnabled(True) + self.form.pb_stop_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(True) + self.form.pb_write_mesh.setEnabled(True) + self.form.pb_check_mesh.setEnabled(True) + self.form.pb_load_mesh.setEnabled(True) + + if self.useRemoteProcessing: + pass + #self.form.pb_run_remote_mesh.setEnabled(True) + #self.form.pb_stop_remote_mesh.setEnabled(False) + #self.form.pb_write_remote_mesh.setEnabled(True) + + else: + self.consoleMessage("Meshing exited with error", 'Error') + self.form.pb_run_mesh.setEnabled(True) + self.form.pb_stop_mesh.setEnabled(False) + self.form.pb_write_mesh.setEnabled(True) + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + + if self.useRemoteProcessing: + pass + #self.form.pb_run_remote_mesh.setEnabled(True) + #self.form.pb_stop_remote_mesh.setEnabled(False) + #self.form.pb_write_remote_mesh.setEnabled(True) + + self.error_message = '' + # Get rid of any existing loaded mesh + self.pbClearMeshClicked() + self.updateUI() + + def openParaview(self): + QApplication.setOverrideCursor(Qt.WaitCursor) + case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) + script_name = "pvScriptMesh.py" + try: + CfdTools.startParaview(case_path, script_name, self.consoleMessage) + finally: + QApplication.restoreOverrideCursor() + + def pbLoadMeshClicked(self): + self.consoleMessage("Reading mesh ...", timed=False) + prev_write_mesh = self.analysis_obj.NeedsMeshRewrite + self.mesh_obj.Proxy.cart_mesh.loadSurfMesh() + self.analysis_obj.NeedsMeshRewrite = prev_write_mesh + self.consoleMessage('Triangulated representation of the surface mesh is shown - ', timed=False) + self.consoleMessage("Please view in Paraview for accurate display.\n", timed=False) + + def pbClearMeshClicked(self): + prev_write_mesh = self.analysis_obj.NeedsMeshRewrite + for m in self.mesh_obj.Group: + if m.isDerivedFrom("Fem::FemMeshObject"): + FreeCAD.ActiveDocument.removeObject(m.Name) + self.analysis_obj.NeedsMeshRewrite = prev_write_mesh + FreeCAD.ActiveDocument.recompute() + + def searchPointInMesh(self): + print ("Searching for an internal vector point ...") + # Apply latest mesh size + self.store() + pointCheck = self.mesh_obj.Proxy.cart_mesh.automaticInsidePointDetect() + if pointCheck is not None: + iMPx, iMPy, iMPz = pointCheck + setQuantity(self.form.if_pointInMeshX, str(iMPx) + "mm") + setQuantity(self.form.if_pointInMeshY, str(iMPy) + "mm") + setQuantity(self.form.if_pointInMeshZ, str(iMPz) + "mm") diff --git a/TaskPanelCfdMesh.ui b/TaskPanelCfdMesh.ui new file mode 100644 index 00000000..fd907248 --- /dev/null +++ b/TaskPanelCfdMesh.ui @@ -0,0 +1,611 @@ + + + CfdMesh + + + + 0 + 0 + 512 + 1127 + + + + CFD Mesh + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 14 + + + 8 + + + + + + 75 + true + + + + Mesh Parameters + + + + + + + Base element size: + + + + + + + + + + + + + + + 0 + 0 + + + + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1.000000000000000 + + + 1000000000.000000000000000 + + + mm + + + g + + + + + + + Mesh utility: + + + + + + + + 75 + true + + + + Mesh Parameters + + + + + + + + + + 0 + 0 + + + + QFrame::Panel + + + QFrame::Plain + + + 0 + + + + + + Search + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 50 + false + + + + Point in mesh + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + 0 + + + + 0 + + + 0 + + + + + 3 + + + 0.001000000000000 + + + 1.000000000000000 + + + 0.050000000000000 + + + + + + + No of cells between levels + + + + + + + Relative edge refinement + + + + + + + Edge detection + + + + + + + 1 + + + 100 + + + + + + + Implicit + + + + + + + Explicit + + + + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 2 + + + + + + 0 + 0 + + + + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1.000000000000000 + + + 1000000000.000000000000000 + + + mm + + + g + + + + + + + + 0 + 0 + + + + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1.000000000000000 + + + 1000000000.000000000000000 + + + mm + + + g + + + + + + + + 0 + 0 + + + + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1.000000000000000 + + + 1000000000.000000000000000 + + + mm + + + g + + + + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + + 75 + true + + + + Host + + + + + + + + 0 + 0 + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Stop + + + + + + + Run mesh case + + + + + + + Edit + + + + + + + Write mesh case + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 14 + + + 8 + + + + + Clear surface mesh + + + + + + + Load surface mesh + + + + + + + + 75 + true + + + + Visualisation + + + + + + + Paraview + + + + + + + Check Mesh + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 8 + + + + + Status + + + + + + + + + + 10 + + + + + + + + + + + QTextEdit::NoWrap + + + + + + + + + + + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+
+ + pb_run_mesh + pb_stop_mesh + pb_paraview + pb_load_mesh + pb_clear_mesh + cb_utility + if_max + pb_searchPointInMesh + if_pointInMeshX + if_pointInMeshY + if_pointInMeshZ + te_output + + + +
diff --git a/TaskPanelCfdSolverControl.ui b/TaskPanelCfdSolverControl.ui new file mode 100644 index 00000000..4543ded8 --- /dev/null +++ b/TaskPanelCfdSolverControl.ui @@ -0,0 +1,278 @@ + + + AnalysisControl + + + + 0 + 0 + 612 + 1067 + + + + Analysis control + + + + + + + 75 + true + + + + OpenFOAM Solver + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 14 + + + 6 + + + + + + 0 + 0 + + + + + 75 + true + + + + Host + + + + + + + + 0 + 0 + + + + false + + + + + + -1 + + + + + + + + + 8 + + + 8 + + + 8 + + + 6 + + + 14 + + + 8 + + + + + true + + + Stop + + + + + + + true + + + Run OF case + + + + + + + true + + + Edit + + + + + + + Write OF case + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + 8 + + + 8 + + + 8 + + + 6 + + + 14 + + + 8 + + + + + + 75 + true + + + + Results + + + + + + + + 75 + true + + + + + + + + + + + true + + + Paraview + + + + + + + + + Status + + + + + + + + 0 + 120 + + + + QTextEdit::NoWrap + + + + + + + + + 8 + + + 6 + + + 8 + + + 6 + + + 8 + + + + + + 10 + + + + + + + + + + + + + + From 2bc607afce632ed0abd1b93bd97b5ff28d64a8d3 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:32:06 -0600 Subject: [PATCH 02/20] Delete CfdCaseWriterFoam.py Uploaded to wrong dir --- CfdCaseWriterFoam.py | 746 ------------------------------------------- 1 file changed, 746 deletions(-) delete mode 100644 CfdCaseWriterFoam.py diff --git a/CfdCaseWriterFoam.py b/CfdCaseWriterFoam.py deleted file mode 100644 index e049bfb7..00000000 --- a/CfdCaseWriterFoam.py +++ /dev/null @@ -1,746 +0,0 @@ -# *************************************************************************** -# * * -# * Copyright (c) 2017 Alfred Bogaers (CSIR) * -# * Copyright (c) 2017 Johan Heyns (CSIR) * -# * Copyright (c) 2017 Oliver Oxtoby (CSIR) * -# * Copyright (c) 2019-2022 Oliver Oxtoby * -# * Copyright (c) 2022 Jonathan Bergh * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License as * -# * published by the Free Software Foundation, either version 3 of the * -# * License, or (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Lesser General Public * -# * License along with this program. If not, * -# * see . * -# * * -# *************************************************************************** - -import os -import os.path -#import FreeCAD -from FreeCAD import Units, ParamGet -from CfdOF import CfdTools -from CfdOF.TemplateBuilder import TemplateBuilder -from CfdOF.CfdTools import cfdMessage -from CfdOF.Mesh import CfdMeshTools - - -class CfdCaseWriterFoam: - def __init__(self, analysis_obj): - self.case_folder = None - self.mesh_file_name = None - self.template_path = None - - self.analysis_obj = analysis_obj - self.solver_obj = CfdTools.getSolver(analysis_obj) - self.physics_model = CfdTools.getPhysicsModel(analysis_obj) - self.mesh_obj = CfdTools.getMesh(analysis_obj) - self.material_objs = CfdTools.getMaterials(analysis_obj) - self.bc_group = CfdTools.getCfdBoundaryGroup(analysis_obj) - self.initial_conditions = CfdTools.getInitialConditions(analysis_obj) - self.reporting_functions = CfdTools.getReportingFunctionsGroup(analysis_obj) - self.scalar_transport_objs = CfdTools.getScalarTransportFunctionsGroup(analysis_obj) - self.porous_zone_objs = CfdTools.getPorousZoneObjects(analysis_obj) - self.initialisation_zone_objs = CfdTools.getInitialisationZoneObjects(analysis_obj) - self.zone_objs = CfdTools.getZoneObjects(analysis_obj) - self.dynamic_mesh_refinement_obj = CfdTools.getDynamicMeshAdaptation(analysis_obj) - self.mesh_generated = False - self.working_dir = CfdTools.getOutputPath(self.analysis_obj) - self.progressCallback = None - - self.settings = None - - def writeCase(self, profile_name): - """ writeCase() will collect case settings, and finally build a runnable case. """ - cfdMessage("Writing case to folder {}\n".format(self.working_dir)) - if not os.path.exists(self.working_dir): - raise IOError("Path " + self.working_dir + " does not exist.") - - # Perform initialisation here rather than __init__ in case of path changes - self.case_folder = os.path.join(self.working_dir, self.solver_obj.InputCaseName) - self.case_folder = os.path.expanduser(os.path.abspath(self.case_folder)) - self.mesh_file_name = os.path.join(self.case_folder, self.solver_obj.InputCaseName, u".unv") - self.template_path = os.path.join(CfdTools.getModulePath(), "Data", "Templates", "case") - - # Collect settings into single dictionary - if not self.mesh_obj: - raise RuntimeError("No mesh object found in analysis") - phys_settings = CfdTools.propsToDict(self.physics_model) - - # Validate BC labels - bc_labels = [b.Label for b in self.bc_group] - for i, l in enumerate(bc_labels): - if bc_labels[i].find(' ') >= 0: - raise ValueError("Boundary condition label '" + bc_labels[i] + "' is not valid: May not contain spaces") - for i, l in enumerate(bc_labels): - for j in range(i+1, len(bc_labels)): - if bc_labels[j] == l: - raise ValueError("Boundary condition label '" + bc_labels[i] + "' is duplicated") - - self.settings = { - 'physics': phys_settings, - 'fluidProperties': [], # Order is important, so use a list - 'initialValues': CfdTools.propsToDict(self.initial_conditions), - 'boundaries': dict((b.Label, CfdTools.propsToDict(b)) for b in self.bc_group), - 'reportingFunctions': dict((fo.Label, CfdTools.propsToDict(fo)) for fo in self.reporting_functions), - 'reportingFunctionsEnabled': False, - 'scalarTransportFunctions': dict((st.Label, CfdTools.propsToDict(st)) for st in self.scalar_transport_objs), - 'scalarTransportFunctionsEnabled': False, - 'dynamicMesh': {}, - 'dynamicMeshEnabled': False, - 'bafflesPresent': self.bafflesPresent(), - 'porousZones': {}, - 'porousZonesPresent': False, - 'initialisationZones': {o.Label: CfdTools.propsToDict(o) for o in self.initialisation_zone_objs}, - 'initialisationZonesPresent': len(self.initialisation_zone_objs) > 0, - 'zones': {o.Label: {'PartNameList': tuple(r[0].Name for r in o.ShapeRefs)} for o in self.zone_objs}, - 'zonesPresent': len(self.zone_objs) > 0, - 'meshType': self.mesh_obj.Proxy.Type, - 'meshDimension': self.mesh_obj.ElementDimension, - 'meshDir': os.path.join(self.working_dir, self.mesh_obj.CaseName), - 'solver': CfdTools.propsToDict(self.solver_obj), - 'system': {}, - 'runChangeDictionary': False - } - - if CfdTools.DockerContainer.usedocker: - mesh_d = self.settings['meshDir'].split(os.sep) - self.settings['meshDir'] = '/tmp/{}'.format(mesh_d[-1]) - - self.processSystemSettings() - self.processSolverSettings() - self.processFluidProperties() - self.processBoundaryConditions() - self.processInitialConditions() - CfdTools.clearCase(self.case_folder) - - self.exportZoneStlSurfaces() - if self.porous_zone_objs: - self.processPorousZoneProperties() - self.processInitialisationZoneProperties() - - if self.reporting_functions: - cfdMessage('Reporting functions present') - self.processReportingFunctions() - - if self.scalar_transport_objs: - cfdMessage('Scalar transport functions present') - self.processScalarTransportFunctions() - - if self.dynamic_mesh_refinement_obj: - cfdMessage('Dynamic mesh adaptation rule present') - self.processDynamicMeshRefinement() - - self.settings['createPatchesFromSnappyBaffles'] = False - cfdMessage("Matching boundary conditions ...\n") - if self.progressCallback: - self.progressCallback("Matching boundary conditions ...") - self.setupPatchNames() - - TemplateBuilder(self.case_folder, self.template_path, self.settings) - - # Update Allrun permission - will fail silently on Windows - file_name = os.path.join(self.case_folder, "Allrun") - import stat - s = os.stat(file_name) - os.chmod(file_name, s.st_mode | stat.S_IEXEC) - - cfdMessage("Successfully wrote case to folder {}\n".format(self.working_dir)) - if self.progressCallback: - self.progressCallback("Case written locally successfully") - - # if using a remote host, copy the case folder from the local case dir - # to the remote host's directory - if profile_name != "local": - profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + profile_name - remote_user = ParamGet(profile_prefs).GetString("Username", "") - remote_hostname = ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = ParamGet(profile_prefs).GetString("OutputPath","") - - #print("remote_user:" + remote_user) - #print("remote_hostname:" + remote_hostname) - #print("remote_output_path:" + remote_output_path) - #print("self.case_folder:" + self.case_folder) - #print("self.working_dir:" + self.working_dir) - - # rsync the meshCase directory to the remote host's output directory - # Typical useage: rsync -r --delete /tmp/ me@david:/tmp - try: - CfdTools.runFoamCommand("rsync -r --delete " + self.case_folder + " " + remote_user + "@" + remote_hostname + \ - ":" + remote_output_path) - except Exception as e: - CfdTools.cfdMessage("Could not copy case to remote host: " + str(e)) - if self.progressCallback: - self.progressCallback("Could not copy case to remote host: " + str(e)) - return False - else: - CfdTools.cfdMessage("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n" ) - if self.progressCallback: - self.progressCallback("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n") - - return True - - def getSolverName(self): - """ - Solver name is selected based on selected physics. This should only be extended as additional physics are - included. - """ - solver = None - if self.physics_model.Phase == 'Single': - if len(self.material_objs) == 1: - if self.physics_model.Flow == 'Incompressible': - if self.physics_model.Thermal == 'None': - if self.physics_model.Time == 'Transient': - solver = 'pimpleFoam' - else: - if self.porous_zone_objs or self.porousBafflesPresent(): - solver = 'porousSimpleFoam' - else: - solver = 'simpleFoam' - else: - raise RuntimeError("Only isothermal simulation currently supported for incompressible flow.") - elif self.physics_model.Flow == 'Compressible': - if self.physics_model.Time == 'Transient': - solver = 'buoyantPimpleFoam' - else: - solver = 'buoyantSimpleFoam' - elif self.physics_model.Flow == 'HighMachCompressible': - solver = 'hisa' - else: - raise RuntimeError(self.physics_model.Flow + " flow model currently not supported.") - else: - raise RuntimeError("Only one material object may be present for single phase simulation.") - elif self.physics_model.Phase == 'FreeSurface': - if self.physics_model.Time == 'Transient': - if self.physics_model.Thermal == 'None': - if len(self.material_objs) == 2: - solver = 'interFoam' - elif len(self.material_objs) > 2: - solver = 'multiphaseInterFoam' - else: - raise RuntimeError("At least two material objects must be present for free surface simulation.") - else: - raise RuntimeError("Only isothermal analysis is currently supported for free surface flow simulation.") - else: - raise RuntimeError("Only transient analysis is supported for free surface flow simulation.") - else: - raise RuntimeError(self.physics_model.Phase + " phase model currently not supported.") - - # Catch-all in case - if solver is None: - raise RuntimeError("No solver is supported to handle the selected physics with {} phases.".format( - len(self.material_objs))) - return solver - - def processSolverSettings(self): - solver_settings = self.settings['solver'] - if solver_settings['Parallel']: - if solver_settings['ParallelCores'] < 2: - solver_settings['ParallelCores'] = 2 - solver_settings['SolverName'] = self.getSolverName() - - def processSystemSettings(self): - system_settings = self.settings['system'] - system_settings['FoamRuntime'] = CfdTools.getFoamRuntime() - system_settings['CasePath'] = self.case_folder - system_settings['FoamPath'] = CfdTools.getFoamDir() - if CfdTools.getFoamRuntime() != 'WindowsDocker': - system_settings['TranslatedFoamPath'] = CfdTools.translatePath(CfdTools.getFoamDir()) - - def setupMesh(self, updated_mesh_path, scale): - if os.path.exists(updated_mesh_path): - CfdTools.convertMesh(self.case_folder, updated_mesh_path, scale) - - def processFluidProperties(self): - # self.material_obj currently stores everything as a string - # Convert to (mostly) SI numbers for OpenFOAM - settings = self.settings - for material_obj in self.material_objs: - mp = material_obj.Material - mp['Name'] = material_obj.Label - if 'Density' in mp: - mp['Density'] = Units.Quantity(mp['Density']).getValueAs("kg/m^3").Value - if 'DynamicViscosity' in mp: - if self.physics_model.Turbulence == 'Inviscid': - mp['DynamicViscosity'] = 0.0 - else: - mp['DynamicViscosity'] = Units.Quantity(mp['DynamicViscosity']).getValueAs("kg/m/s").Value - mp['KinematicViscosity'] = mp['DynamicViscosity']/mp['Density'] - if 'MolarMass' in mp: - # OpenFOAM uses kg/kmol - mp['MolarMass'] = Units.Quantity(mp['MolarMass']).getValueAs("kg/mol").Value*1000 - if 'Cp' in mp: - mp['Cp'] = Units.Quantity(mp['Cp']).getValueAs("J/kg/K").Value - if 'SutherlandTemperature' in mp: - mp['SutherlandTemperature'] = Units.Quantity(mp['SutherlandTemperature']).getValueAs("K").Value - if 'SutherlandRefViscosity' in mp and 'SutherlandRefTemperature' in mp: - mu0 = Units.Quantity(mp['SutherlandRefViscosity']).getValueAs("kg/m/s").Value - T0 = Units.Quantity(mp['SutherlandRefTemperature']).getValueAs("K").Value - mp['SutherlandConstant'] = mu0/T0**(3./2)*(T0+mp['SutherlandTemperature']) - for k in mp: - if k.endswith('Polynomial'): - poly = mp[k].split() - poly8 = [0.0]*8 - for i in range(len(poly)): - try: - poly8[i] = float(poly[i]) - except ValueError: - raise ValueError("Invalid coefficient {} in polynomial coefficient {}".format(poly[i], k)) - mp[k] = ' '.join(str(v) for v in poly8) - - settings['fluidProperties'].append(mp) - - def processBoundaryConditions(self): - """ Compute any quantities required before case build """ - settings = self.settings - # Copy keys so that we can delete while iterating - bc_names = list(settings['boundaries'].keys()) - for bc_name in bc_names: - bc = settings['boundaries'][bc_name] - if not bc['VelocityIsCartesian']: - veloMag = bc['VelocityMag'] - face = bc['DirectionFace'].split(':') - if not face[0]: - face = bc['ShapeRefs'][0].Name - # See if entered face actually exists and is planar - try: - selected_object = self.analysis_obj.Document.getObject(face[0]) - if hasattr(selected_object, "Shape"): - elt = selected_object.Shape.getElement(face[1]) - if elt.ShapeType == 'Face' and CfdTools.isPlanar(elt): - n = elt.normalAt(0.5, 0.5) - if bc['ReverseNormal']: - n = [-ni for ni in n] - velocity = [ni*veloMag for ni in n] - bc['Ux'] = velocity[0] - bc['Uy'] = velocity[1] - bc['Uz'] = velocity[2] - else: - raise RuntimeError - else: - raise RuntimeError - except (SystemError, RuntimeError): - raise RuntimeError(str(bc['DirectionFace']) + " is not a valid, planar face.") - if settings['solver']['SolverName'] in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam']: - bc['KinematicPressure'] = bc['Pressure']/settings['fluidProperties'][0]['Density'] - - if bc['PorousBaffleMethod'] == 'porousScreen': - wireDiam = bc['ScreenWireDiameter'] - spacing = bc['ScreenSpacing'] - CD = 1.0 # Drag coeff of wire (Simmons - valid for Re > ~300) - beta = (1-wireDiam/spacing)**2 - bc['PressureDropCoeff'] = CD*(1-beta) - - if settings['solver']['SolverName'] in ['interFoam', 'multiphaseInterFoam']: - # Make sure the first n-1 alpha values exist, and write the n-th one - # consistently for multiphaseInterFoam - sum_alpha = 0.0 - alphas_new = {} - for i, m in enumerate(settings['fluidProperties']): - alpha_name = m['Name'] - if i == len(settings['fluidProperties']) - 1: - if settings['solver']['SolverName'] == 'multiphaseInterFoam': - alphas_new[alpha_name] = 1.0 - sum_alpha - else: - alpha = Units.Quantity(bc.get('VolumeFractions', {}).get(alpha_name, '0')).Value - alphas_new[alpha_name] = alpha - sum_alpha += alpha - bc['VolumeFractions'] = alphas_new - - # Copy turbulence settings - bc['TurbulenceIntensity'] = bc['TurbulenceIntensityPercentage']/100.0 - physics = settings['physics'] - if physics['Turbulence'] == 'RANS' and physics['TurbulenceModel'] == 'SpalartAllmaras': - if (bc['BoundaryType'] == 'inlet' or bc['BoundaryType'] == 'open') and \ - bc['TurbulenceInletSpecification'] == 'intensityAndLengthScale': - if bc['BoundarySubType'] == 'uniformVelocityInlet' or bc['BoundarySubType'] == 'farField': - Uin = (bc['Ux']**2 + bc['Uy']**2 + bc['Uz']**2)**0.5 - - # Turb Intensity and length scale - I = bc['TurbulenceIntensity'] - l = bc['TurbulenceLengthScale'] - - # Spalart Allmaras - bc['NuTilda'] = (3.0/2.0)**0.5 * Uin * I * l - - else: - raise RuntimeError( - "Inlet type currently unsupported for calculating turbulence inlet conditions from " - "intensity and length scale.") - - if bc['DefaultBoundary']: - if settings['boundaries'].get('defaultFaces'): - raise ValueError("More than one default boundary defined") - settings['boundaries']['defaultFaces'] = bc - if not settings['boundaries'].get('defaultFaces'): - settings['boundaries']['defaultFaces'] = { - 'BoundaryType': 'wall', - 'BoundarySubType': 'slipWall', - 'ThermalBoundaryType': 'zeroGradient' - } - - # Assign any extruded patches as the appropriate type - mr_objs = CfdTools.getMeshRefinementObjs(self.mesh_obj) - for mr_id, mr_obj in enumerate(mr_objs): - if mr_obj.Extrusion and mr_obj.ExtrusionType == "2DPlanar": - settings['boundaries'][mr_obj.Label] = { - 'BoundaryType': 'constraint', - 'BoundarySubType': 'empty' - } - settings['boundaries'][mr_obj.Label+"BackFace"] = { - 'BoundaryType': 'constraint', - 'BoundarySubType': 'empty' - } - if mr_obj.Extrusion and mr_obj.ExtrusionType == "2DWedge": - settings['boundaries'][mr_obj.Label] = { - 'BoundaryType': 'constraint', - 'BoundarySubType': 'symmetry' - } - settings['boundaries'][mr_obj.Label+"BackFace"] = { - 'BoundaryType': 'constraint', - 'BoundarySubType': 'symmetry' - } - - def parseFaces(self, shape_refs): - pass - - def processInitialConditions(self): - """ Do any required computations before case build. Boundary conditions must be processed first. """ - settings = self.settings - initial_values = settings['initialValues'] - if settings['solver']['SolverName'] in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam']: - mat_prop = settings['fluidProperties'][0] - initial_values['KinematicPressure'] = initial_values['Pressure'] / mat_prop['Density'] - if settings['solver']['SolverName'] in ['interFoam', 'multiphaseInterFoam']: - # Make sure the first n-1 alpha values exist, and write the n-th one - # consistently for multiphaseInterFoam - sum_alpha = 0.0 - alphas_new = {} - for i, m in enumerate(settings['fluidProperties']): - alpha_name = m['Name'] - if i == len(settings['fluidProperties'])-1: - if settings['solver']['SolverName'] == 'multiphaseInterFoam': - alphas_new[alpha_name] = 1.0-sum_alpha - else: - alpha = Units.Quantity(initial_values.get('VolumeFractions', {}).get(alpha_name, '0')).Value - alphas_new[alpha_name] = alpha - sum_alpha += alpha - initial_values['VolumeFractions'] = alphas_new - - if initial_values['PotentialFlowP']: - if settings['solver']['SolverName'] not in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam', 'hisa']: - raise RuntimeError("Selected solver does not support potential pressure initialisation.") - - physics = settings['physics'] - - # Copy velocity - if initial_values['UseInletUValues']: - if initial_values['BoundaryU']: - inlet_bc = settings['boundaries'][initial_values['BoundaryU'].Label] - if inlet_bc['BoundarySubType'] == 'uniformVelocityInlet' or inlet_bc['BoundarySubType'] == 'farField': - initial_values['Ux'] = inlet_bc['Ux'] - initial_values['Uy'] = inlet_bc['Uy'] - initial_values['Uz'] = inlet_bc['Uz'] - else: - raise RuntimeError("Boundary type not appropriate to determine initial velocity.") - else: - raise RuntimeError("No boundary selected to copy initial velocity value from.") - - # Copy pressure - if initial_values['UseOutletPValue']: - if initial_values['BoundaryP']: - outlet_bc = settings['boundaries'][initial_values['BoundaryP'].Label] - if outlet_bc['BoundarySubType'] == 'staticPressureOutlet' or \ - outlet_bc['BoundarySubType'] == 'totalPressureOpening' or \ - outlet_bc['BoundarySubType'] == 'totalPressureInlet' or \ - outlet_bc['BoundarySubType'] == 'staticPressureInlet' or \ - outlet_bc['BoundarySubType'] == 'farField': - initial_values['Pressure'] = outlet_bc['Pressure'] - else: - raise RuntimeError("Boundary type not appropriate to determine initial pressure.") - else: - raise RuntimeError("No boundary selected to copy initial pressure value from.") - - if physics['Thermal'] == 'Energy' and initial_values['UseInletTemperatureValue']: - inlet_bc = settings['boundaries'][initial_values['BoundaryT'].Label] - if inlet_bc['BoundaryType'] == 'inlet': - if inlet_bc['ThermalBoundaryType'] == 'fixedValue': - initial_values['Temperature'] = inlet_bc['Temperature'] - else: - raise RuntimeError("Inlet type not appropriate to determine initial temperature") - elif inlet_bc['BoundarySubType'] == 'farField': - initial_values['Temperature'] = inlet_bc['Temperature'] - else: - raise RuntimeError("Inlet type not appropriate to determine initial temperature.") - - # Copy turbulence settings - if physics['TurbulenceModel'] is not None: - if initial_values['UseInletTurbulenceValues']: - if initial_values['BoundaryTurb']: - inlet_bc = settings['boundaries'][initial_values['BoundaryTurb'].Label] - - if inlet_bc['TurbulenceInletSpecification'] == 'TKEAndSpecDissipationRate': - initial_values['k'] = inlet_bc['TurbulentKineticEnergy'] - initial_values['omega'] = inlet_bc['SpecificDissipationRate'] - elif inlet_bc['TurbulenceInletSpecification'] == 'TKEAndDissipationRate': - initial_values['k'] = inlet_bc['TurbulentKineticEnergy'] - initial_values['epsilon'] = inlet_bc['DissipationRate'] - elif inlet_bc['TurbulenceInletSpecification'] == 'TransportedNuTilda': - initial_values['nuTilda'] = inlet_bc['NuTilda'] - elif inlet_bc['TurbulenceInletSpecification'] == 'TKESpecDissipationRateGammaAndReThetat': - initial_values['k'] = inlet_bc['TurbulentKineticEnergy'] - initial_values['omega'] = inlet_bc['SpecificDissipationRate'] - initial_values['gammaInt'] = inlet_bc['Intermittency'] - initial_values['ReThetat'] = inlet_bc['ReThetat'] - elif inlet_bc['TurbulenceInletSpecification'] == 'TurbulentViscosity': - initial_values['nut'] = inlet_bc['TurbulentViscosity'] - elif inlet_bc['TurbulenceInletSpecification'] == 'TurbulentViscosityAndK': - initial_values['kEqnk'] = inlet_bc['kEqnTurbulentKineticEnergy'] - initial_values['kEqnNut'] = inlet_bc['kEqnTurbulentViscosity'] - elif inlet_bc['TurbulenceInletSpecification'] == 'intensityAndLengthScale': - if inlet_bc['BoundarySubType'] == 'uniformVelocityInlet' or \ - inlet_bc['BoundarySubType'] == 'farField': - Uin = (inlet_bc['Ux']**2 + - inlet_bc['Uy']**2 + - inlet_bc['Uz']**2)**0.5 - - # Turb Intensity (or Tu) and length scale - I = inlet_bc['TurbulenceIntensityPercentage'] / 100.0 # Convert from percent to fraction - l = inlet_bc['TurbulenceLengthScale'] - Cmu = 0.09 # Standard turbulence model parameter - - # k omega, k epsilon - k = 3.0/2.0*(Uin*I)**2 - omega = k**0.5/(Cmu**0.25*l) - epsilon = (k**(3.0/2.0) * Cmu**0.75) / l - - # Spalart Allmaras - nuTilda = inlet_bc['NuTilda'] - - # k omega (transition) - gammaInt = 1 - if I <= 1.3: - ReThetat = 1173.51 - (589.428 * I) + (0.2196 / (I**2)) - else: - ReThetat = 331.5 / ((I - 0.5658)**0.671) - - # Set the values - initial_values['k'] = k - initial_values['omega'] = omega - initial_values['epsilon'] = epsilon - initial_values['nuTilda'] = nuTilda - initial_values['gammaInt'] = gammaInt - initial_values['ReThetat'] = ReThetat - - else: - raise RuntimeError( - "Inlet type currently unsupported for copying turbulence initial conditions.") - else: - raise RuntimeError( - "Turbulence inlet specification currently unsupported for copying turbulence initial conditions") - else: - raise RuntimeError("No boundary selected to copy initial turbulence values from.") - #TODO: Check that the required values have actually been set for each turbulent model - - # Function objects (reporting functions, probes) - def processReportingFunctions(self): - """ Compute any Function objects required before case build """ - settings = self.settings - settings['reportingFunctionsEnabled'] = True - - for name in settings['reportingFunctions']: - rf = settings['reportingFunctions'][name] - - rf['PatchName'] = rf['Patch'].Label - rf['CentreOfRotation'] = \ - tuple(Units.Quantity(p, Units.Length).getValueAs('m') for p in rf['CentreOfRotation']) - rf['Direction'] = tuple(p for p in rf['Direction']) - - if rf['ReportingFunctionType'] == 'ForceCoefficients': - pitch_axis = rf['Lift'].cross(rf['Drag']) - rf['Lift'] = tuple(p for p in rf['Lift']) - rf['Drag'] = tuple(p for p in rf['Drag']) - rf['Pitch'] = tuple(p for p in pitch_axis) - - settings['reportingFunctions'][name]['ProbePosition'] = tuple( - Units.Quantity(p, Units.Length).getValueAs('m') - for p in settings['reportingFunctions'][name]['ProbePosition']) - - def processScalarTransportFunctions(self): - settings = self.settings - settings['scalarTransportFunctionsEnabled'] = True - for name in settings['scalarTransportFunctions']: - stf = settings['scalarTransportFunctions'][name] - if settings['solver']['SolverName'] in ['simpleFoam', 'porousSimpleFoam', 'pimpleFoam']: - stf['InjectionRate'] = stf['InjectionRate']/settings['fluidProperties'][0]['Density'] - stf['DiffusivityFixedValue'] = stf['DiffusivityFixedValue']/settings['fluidProperties'][0]['Density'] - stf['InjectionPoint'] = tuple( - Units.Quantity(p, Units.Length).getValueAs('m') for p in stf['InjectionPoint']) - - # Mesh related - def processDynamicMeshRefinement(self): - settings = self.settings - settings['dynamicMeshEnabled'] = True - - # Check whether transient - if not self.physics_model.Time == 'Transient': - raise RuntimeError("Dynamic mesh refinement is not supported by steady-state solvers") - - # Check whether cellLevel supported - if self.mesh_obj.MeshUtility not in ['cfMesh', 'snappyHexMesh']: - raise RuntimeError("Dynamic mesh refinement is only supported by cfMesh and snappyHexMesh") - - # Check whether 2D extrusion present - mesh_refinements = CfdTools.getMeshRefinementObjs(self.mesh_obj) - for mr in mesh_refinements: - if mr.Extrusion: - if mr.ExtrusionType == '2DPlanar' or mr.ExtrusionType == '2DWedge': - raise RuntimeError("Dynamic mesh refinement will not work with 2D or wedge mesh") - - settings['dynamicMesh'] = CfdTools.propsToDict(self.dynamic_mesh_refinement_obj) - - # Zones - def exportZoneStlSurfaces(self): - for zo in self.zone_objs: - for r in zo.ShapeRefs: - path = os.path.join(self.working_dir, - self.solver_obj.InputCaseName, - "constant", - "triSurface") - if not os.path.exists(path): - os.makedirs(path) - sel_obj = r[0] - shape = sel_obj.Shape - CfdMeshTools.writeSurfaceMeshFromShape(shape, path, r[0].Name, self.mesh_obj) - print("Successfully wrote stl surface\n") - - def processPorousZoneProperties(self): - settings = self.settings - settings['porousZonesPresent'] = True - porousZoneSettings = settings['porousZones'] - for po in self.porous_zone_objs: - pd = {'PartNameList': tuple(r[0].Name for r in po.ShapeRefs)} - po = CfdTools.propsToDict(po) - if po['PorousCorrelation'] == 'DarcyForchheimer': - pd['D'] = (po['D1'], po['D2'], po['D3']) - pd['F'] = (po['F1'], po['F2'], po['F3']) - pd['e1'] = tuple(po['e1']) - pd['e3'] = tuple(po['e3']) - elif po['PorousCorrelation'] == 'Jakob': - # Calculate effective Darcy-Forchheimer coefficients - # This is for equilateral triangles arranged with the triangles pointing in BundleLayerNormal - # direction (direction of greater spacing - sqrt(3)*triangleEdgeLength) - pd['e1'] = tuple(po['SpacingDirection']) # OpenFOAM modifies to be orthog to e3 - pd['e3'] = tuple(po['TubeAxis']) - spacing = po['TubeSpacing'] - d0 = po['OuterDiameter'] - u0 = po['VelocityEstimate'] - aspectRatio = po['AspectRatio'] - kinVisc = self.settings['fluidProperties']['KinematicViscosity'] - if kinVisc == 0.0: - raise ValueError("Viscosity must be set for Jakob correlation") - if spacing < d0: - raise ValueError("Tube spacing may not be less than diameter") - D = [0, 0, 0] - F = [0, 0, 0] - for (i, Sl, St) in [(0, aspectRatio*spacing, spacing), (1, spacing, aspectRatio*spacing)]: - C = 1.0/St*0.5*(1.0+0.47/(Sl/d0-1)**1.06)*(1.0/(1-d0/Sl))**(2.0-0.16) - Di = C/d0*0.5*(u0*d0/kinVisc)**(1.0-0.16) - Fi = C*(u0*d0/kinVisc)**(-0.16) - D[i] = Di - F[i] = Fi - pd['D'] = tuple(D) - pd['F'] = tuple(F) - # Currently assuming zero drag parallel to tube bundle (3rd principal dirn) - else: - raise RuntimeError("Unrecognised method for porous baffle resistance") - porousZoneSettings[po['Label']] = pd - - def processInitialisationZoneProperties(self): - settings = self.settings - if settings['solver']['SolverName'] in ['interFoam', 'multiphaseInterFoam']: - # Make sure the first n-1 alpha values exist, and write the n-th one - # consistently for multiphaseInterFoam - for zone_name in settings['initialisationZones']: - z = settings['initialisationZones'][zone_name] - sum_alpha = 0.0 - if 'VolumeFractions' in z: - alphas_new = {} - for i, m in enumerate(settings['fluidProperties']): - alpha_name = m['Name'] - if i == len(settings['fluidProperties'])-1: - if settings['solver']['SolverName'] == 'multiphaseInterFoam': - alphas_new[alpha_name] = 1.0-sum_alpha - else: - alpha = Units.Quantity(z['VolumeFractions'].get(alpha_name, '0')).Value - alphas_new[alpha_name] = alpha - sum_alpha += alpha - z['VolumeFractions'] = alphas_new - - def bafflesPresent(self): - for b in self.bc_group: - if b.BoundaryType == 'baffle': - return True - return False - - def porousBafflesPresent(self): - for b in self.bc_group: - if b.BoundaryType == 'baffle' and \ - b.BoundarySubType == 'porousBaffle': - return True - return False - - def setupPatchNames(self): - print('Populating createPatchDict to update BC names') - settings = self.settings - settings['createPatches'] = {} - settings['createPatchesSnappyBaffles'] = {} - bc_group = self.bc_group - - defaultPatchType = "patch" - for bc_id, bc_obj in enumerate(bc_group): - bcType = bc_obj.BoundaryType - bcSubType = bc_obj.BoundarySubType - patchType = CfdTools.getPatchType(bcType, bcSubType) - settings['createPatches'][bc_obj.Label] = { - 'PatchNamesList': '"patch_'+str(bc_id+1)+'_.*"', - 'PatchType': patchType - } - if bc_obj.DefaultBoundary: - defaultPatchType = patchType - - if bcType == 'baffle' and self.mesh_obj.MeshUtility == 'snappyHexMesh': - settings['createPatchesFromSnappyBaffles'] = True - settings['createPatchesSnappyBaffles'][bc_obj.Label] = { - 'PatchNamesList': '"'+bc_obj.Name+'_[^_]*"', - 'PatchNamesListSlave': '"'+bc_obj.Name+'_.*_slave"'} - - # Set up default BC for unassigned faces - settings['createPatches']['defaultFaces'] = { - 'PatchNamesList': '"patch_0_0"', - 'PatchType': defaultPatchType - } - - # Assign any extruded patches as the appropriate type - mr_objs = CfdTools.getMeshRefinementObjs(self.mesh_obj) - for mr_id, mr_obj in enumerate(mr_objs): - if mr_obj.Extrusion and mr_obj.ExtrusionType == "2DPlanar": - settings['createPatches'][mr_obj.Label] = { - 'PatchNamesList': '"patch_.*_'+str(mr_id+1)+'"', - 'PatchType': "empty" - } - elif mr_obj.Extrusion and mr_obj.ExtrusionType == "2DWedge": - settings['createPatches'][mr_obj.Label] = { - 'PatchNamesList': '"patch_.*_'+str(mr_id+1)+'"', - 'PatchType': "symmetry" - } - else: - # Add others to default faces list - settings['createPatches']['defaultFaces']['PatchNamesList'] += ' "patch_0_'+str(mr_id+1) + '"' From fd065327fdfe70407d51abe2c27cce94b37bfc9b Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:32:22 -0600 Subject: [PATCH 03/20] Delete CfdPreferencePage.py Uploaded to wrong dir --- CfdPreferencePage.py | 639 ------------------------------------------- 1 file changed, 639 deletions(-) delete mode 100644 CfdPreferencePage.py diff --git a/CfdPreferencePage.py b/CfdPreferencePage.py deleted file mode 100644 index d078672d..00000000 --- a/CfdPreferencePage.py +++ /dev/null @@ -1,639 +0,0 @@ -# *************************************************************************** -# * * -# * Copyright (c) 2017-2018 Oliver Oxtoby (CSIR) * -# * Copyright (c) 2017 Johan Heyns (CSIR) * -# * Copyright (c) 2017 Alfred Bogaers (CSIR) * -# * Copyright (c) 2019-2022 Oliver Oxtoby * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License as * -# * published by the Free Software Foundation, either version 3 of the * -# * License, or (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Lesser General Public * -# * License along with this program. If not, * -# * see . * -# * * -# *************************************************************************** - -import os -import os.path -import platform -import sys -if sys.version_info >= (3,): # Python 3 - import urllib.request as urlrequest - import urllib.parse as urlparse -else: - import urllib as urlrequest - import urlparse - -import ssl -import ctypes -import FreeCAD -from CfdOF import CfdTools -import tempfile -from contextlib import closing -from xml.sax.saxutils import escape - -if FreeCAD.GuiUp: - import FreeCADGui - from PySide import QtCore - from PySide import QtGui - from PySide.QtCore import Qt, QObject, QThread - from PySide.QtGui import QApplication - - -# Constants -OPENFOAM_URL = \ - "https://sourceforge.net/projects/openfoam/files/v2206/OpenFOAM-v2206-windows-mingw.exe/download" -OPENFOAM_FILE_EXT = ".exe" -PARAVIEW_URL = \ - "https://www.paraview.org/paraview-downloads/download.php?submit=Download&version=v5.10&type=binary&os=Windows&downloadFile=ParaView-5.10.1-Windows-Python3.9-msvc2017-AMD64.exe" -PARAVIEW_FILE_EXT = ".exe" -CFMESH_URL = \ - "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof.zip/download" -CFMESH_URL_MINGW = \ - "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof-binaries-{}.zip/download" -CFMESH_FILE_BASE = "cfmesh-cfdof" -CFMESH_FILE_EXT = ".zip" -HISA_URL = \ - "https://sourceforge.net/projects/hisa/files/hisa-master.zip/download" -HISA_URL_MINGW = \ - "https://sourceforge.net/projects/hisa/files/hisa-master-binaries-{}.zip/download" -HISA_FILE_BASE = "hisa-master" -HISA_FILE_EXT = ".zip" -DOCKER_URL = \ - "docker.io/mmcker/cfdof-openfoam" - -# Tasks for the worker thread -DOWNLOAD_OPENFOAM = 1 -DOWNLOAD_PARAVIEW = 2 -DOWNLOAD_CFMESH = 3 -DOWNLOAD_HISA = 4 -DOWNLOAD_DOCKER = 5 - -class CloseDetector(QObject): - def __init__(self, obj, callback): - super().__init__(obj) - self.callback = callback - - def eventFilter(self, obj, event): - if event.type() == QtCore.QEvent.ChildRemoved: - self.callback() - return False - - -class CfdPreferencePage: - def __init__(self): - ui_path = os.path.join(CfdTools.getModulePath(), 'Gui', "CfdPreferencePage.ui") - self.form = FreeCADGui.PySideUic.loadUi(ui_path) - - self.form.tb_choose_foam_dir.clicked.connect(self.chooseFoamDir) - self.form.le_foam_dir.textChanged.connect(self.foamDirChanged) - self.form.tb_choose_paraview_path.clicked.connect(self.chooseParaviewPath) - self.form.le_paraview_path.textChanged.connect(self.paraviewPathChanged) - self.form.tb_choose_gmsh_path.clicked.connect(self.chooseGmshPath) - self.form.le_gmsh_path.textChanged.connect(self.gmshPathChanged) - self.form.pb_run_dependency_checker.clicked.connect(self.runDependencyChecker) - self.form.pb_download_install_openfoam.clicked.connect(self.downloadInstallOpenFoam) - self.form.tb_pick_openfoam_file.clicked.connect(self.pickOpenFoamFile) - self.form.pb_download_install_paraview.clicked.connect(self.downloadInstallParaview) - self.form.tb_pick_paraview_file.clicked.connect(self.pickParaviewFile) - self.form.pb_download_install_cfMesh.clicked.connect(self.downloadInstallCfMesh) - self.form.tb_pick_cfmesh_file.clicked.connect(self.pickCfMeshFile) - self.form.pb_download_install_hisa.clicked.connect(self.downloadInstallHisa) - self.form.tb_pick_hisa_file.clicked.connect(self.pickHisaFile) - - self.form.le_openfoam_url.setText(OPENFOAM_URL) - self.form.le_paraview_url.setText(PARAVIEW_URL) - - self.form.tb_choose_output_dir.clicked.connect(self.chooseOutputDir) - self.form.le_output_dir.textChanged.connect(self.outputDirChanged) - - self.form.cb_docker_sel.clicked.connect(self.dockerCheckboxClicked) - self.form.pb_download_install_docker.clicked.connect(self.downloadInstallDocker) - - self.dockerCheckboxClicked() - - self.ev_filter = CloseDetector(self.form, self.cleanUp) - self.form.installEventFilter(self.ev_filter) - - self.thread = None - self.install_process = None - - self.console_message = "" - - self.foam_dir = "" - self.initial_foam_dir = "" - - self.paraview_path = "" - self.initial_paraview_path = "" - - self.gmsh_path = "" - self.initial_gmsh_path = "" - - self.output_dir = "" - - self.form.gb_openfoam.setVisible(platform.system() == 'Windows') - self.form.gb_paraview.setVisible(platform.system() == 'Windows') - - def __del__(self): - self.cleanUp() - - def cleanUp(self): - if self.thread and self.thread.isRunning(): - FreeCAD.Console.PrintError("Terminating a pending install task\n") - self.thread.quit = True - if self.install_process and self.install_process.state() == QtCore.QProcess.Running: - FreeCAD.Console.PrintError("Terminating a pending install task\n") - self.install_process.terminate() - QApplication.restoreOverrideCursor() - - def saveSettings(self): - CfdTools.setFoamDir(self.foam_dir) - CfdTools.setParaviewPath(self.paraview_path) - CfdTools.setGmshPath(self.gmsh_path) - prefs = CfdTools.getPreferencesLocation() - FreeCAD.ParamGet(prefs).SetString("DefaultOutputPath", self.output_dir) - FreeCAD.ParamGet(prefs).SetBool("UseDocker",self.form.cb_docker_sel.isChecked()) - FreeCAD.ParamGet(prefs).SetString("DockerURL",self.form.le_docker_url.text()) - - def loadSettings(self): - # Don't set the autodetected location, since the user might want to allow that to vary according - # to WM_PROJECT_DIR setting - prefs = CfdTools.getPreferencesLocation() - self.foam_dir = FreeCAD.ParamGet(prefs).GetString("InstallationPath", "") - self.initial_foam_dir = str(self.foam_dir) - self.form.le_foam_dir.setText(self.foam_dir) - - self.paraview_path = CfdTools.getParaviewPath() - self.initial_paraview_path = str(self.paraview_path) - self.form.le_paraview_path.setText(self.paraview_path) - - self.gmsh_path = CfdTools.getGmshPath() - self.initial_gmsh_path = str(self.gmsh_path) - self.form.le_gmsh_path.setText(self.gmsh_path) - - self.output_dir = CfdTools.getDefaultOutputPath() - self.form.le_output_dir.setText(self.output_dir) - - if FreeCAD.ParamGet(prefs).GetBool("UseDocker", 0): - self.form.cb_docker_sel.setCheckState(Qt.Checked) - - # Set usedocker and enable/disable download buttons - self.dockerCheckboxClicked() - - self.form.le_docker_url.setText(FreeCAD.ParamGet(prefs).GetString("DockerURL", DOCKER_URL)) - - self.setDownloadURLs() - - def consoleMessage(self, message="", colour_type=None): - message = escape(message) - message = message.replace('\n', '
') - if colour_type: - self.console_message += '{1}
'.format(CfdTools.getColour(colour_type), message) - else: - self.console_message += message+'
' - self.form.textEdit_Output.setText(self.console_message) - self.form.textEdit_Output.moveCursor(QtGui.QTextCursor.End) - - def foamDirChanged(self, text): - self.foam_dir = text - if self.foam_dir and os.access(self.foam_dir, os.R_OK): - self.setDownloadURLs() - - def testGetRuntime(self, disable_exception=True): - """ Set the foam dir temporarily and see if we can detect the runtime """ - CfdTools.setFoamDir(self.foam_dir) - try: - runtime = CfdTools.getFoamRuntime() - except IOError as e: - runtime = None - if not disable_exception: - raise - CfdTools.setFoamDir(self.initial_foam_dir) - return runtime - - def setDownloadURLs(self): - if self.testGetRuntime() == "MinGW": - # Temporarily apply the foam dir selection - CfdTools.setFoamDir(self.foam_dir) - foam_ver = os.path.split(CfdTools.getFoamDir())[-1] - self.form.le_cfmesh_url.setText(CFMESH_URL_MINGW.format(foam_ver)) - self.form.le_hisa_url.setText(HISA_URL_MINGW.format(foam_ver)) - CfdTools.setFoamDir(self.initial_foam_dir) - else: - self.form.le_cfmesh_url.setText(CFMESH_URL) - self.form.le_hisa_url.setText(HISA_URL) - - def paraviewPathChanged(self, text): - self.paraview_path = text - - def gmshPathChanged(self, text): - self.gmsh_path = text - - def chooseFoamDir(self): - d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose OpenFOAM directory', self.foam_dir) - if d and os.access(d, os.R_OK): - self.foam_dir = d - self.form.le_foam_dir.setText(self.foam_dir) - - def chooseParaviewPath(self): - p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView executable', self.paraview_path, - filter="*.exe" if platform.system() == 'Windows' else None) - if p and os.access(p, os.R_OK): - self.paraview_path = p - self.form.le_paraview_path.setText(self.paraview_path) - - def chooseGmshPath(self): - p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose gmsh executable', self.gmsh_path, - filter="*.exe" if platform.system() == 'Windows' else None) - if p and os.access(p, os.R_OK): - self.gmsh_path = p - self.form.le_gmsh_path.setText(self.gmsh_path) - - def outputDirChanged(self, text): - self.output_dir = text - - def chooseOutputDir(self): - d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose output directory', self.output_dir) - if d and os.access(d, os.W_OK): - self.output_dir = os.path.abspath(d) - self.form.le_output_dir.setText(self.output_dir) - - def runDependencyChecker(self): - # Temporarily apply the foam dir selection and paraview path selection - CfdTools.setFoamDir(self.foam_dir) - CfdTools.setParaviewPath(self.paraview_path) - CfdTools.setGmshPath(self.gmsh_path) - QApplication.setOverrideCursor(Qt.WaitCursor) - self.consoleMessage("Checking dependencies...") - msg = CfdTools.checkCfdDependencies() - self.consoleMessage(msg) - CfdTools.setFoamDir(self.initial_foam_dir) - CfdTools.setParaviewPath(self.initial_paraview_path) - CfdTools.setGmshPath(self.initial_gmsh_path) - QApplication.restoreOverrideCursor() - - def showAdministratorWarningMessage(self): - if platform.system() == "Windows": - is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 - if not is_admin: - button = QtGui.QMessageBox.question( - None, - "CfdOF Workbench", - "Before installing this software, it is advised to run FreeCAD in administrator mode (hold down " - " the 'Shift' key, right-click on the FreeCAD launcher, and choose 'Run as administrator').\n\n" - "If this is not possible, please make sure OpenFOAM is installed in a location to which you have " - "full read/write access rights.\n\n" - "You are not currently running as administrator - do you wish to continue anyway?") - return button == QtGui.QMessageBox.StandardButton.Yes - return True - - def downloadInstallOpenFoam(self): - if not self.showAdministratorWarningMessage(): - return - if self.createThread(): - self.thread.task = DOWNLOAD_OPENFOAM - self.thread.openfoam_url = self.form.le_openfoam_url.text() - self.thread.start() - - def pickOpenFoamFile(self): - f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose OpenFOAM install file', filter="*.exe") - if f and os.access(f, os.R_OK): - self.form.le_openfoam_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) - - def downloadInstallParaview(self): - if self.createThread(): - self.thread.task = DOWNLOAD_PARAVIEW - self.thread.paraview_url = self.form.le_paraview_url.text() - self.thread.start() - - def pickParaviewFile(self): - f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView install file', filter="*.exe") - if f and os.access(f, os.R_OK): - self.form.le_paraview_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) - - def downloadInstallCfMesh(self): - if not self.showAdministratorWarningMessage(): - return - - runtime = self.testGetRuntime(False) - if runtime == "MinGW" and self.form.le_cfmesh_url.text() == CFMESH_URL: - # Openfoam might have just been installed and the URL would not have had a chance to update - self.setDownloadURLs() - - if self.createThread(): - self.thread.task = DOWNLOAD_CFMESH - # We are forced to apply the foam dir selection - reset when the task finishes - CfdTools.setFoamDir(self.foam_dir) - self.thread.cfmesh_url = self.form.le_cfmesh_url.text() - self.thread.start() - - def pickCfMeshFile(self): - f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose cfMesh archive', filter="*.zip") - if f and os.access(f, os.R_OK): - self.form.le_cfmesh_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) - - def downloadInstallHisa(self): - if not self.showAdministratorWarningMessage(): - return - - runtime = self.testGetRuntime(False) - if runtime == "MinGW" and self.form.le_hisa_url.text() == HISA_URL: - # Openfoam might have just been installed and the URL would not have had a chance to update - self.setDownloadURLs() - - if self.createThread(): - self.thread.task = DOWNLOAD_HISA - # We are forced to apply the foam dir selection - reset when the task finishes - CfdTools.setFoamDir(self.foam_dir) - self.thread.hisa_url = self.form.le_hisa_url.text() - self.thread.start() - - def pickHisaFile(self): - f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose HiSA archive', filter="*.zip") - if f and os.access(f, os.R_OK): - self.form.le_hisa_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) - - def createThread(self): - if self.thread and self.thread.isRunning(): - self.consoleMessage("Busy - please wait...", 'Error') - return False - else: - self.thread = CfdPreferencePageThread() - self.thread.signals.error.connect(self.threadError) - self.thread.signals.finished.connect(self.threadFinished) - self.thread.signals.status.connect(self.threadStatus) - self.thread.signals.downloadProgress.connect(self.downloadProgress) - return True - - def threadStatus(self, msg): - self.consoleMessage(msg) - - def threadError(self, msg): - self.consoleMessage(msg, 'Error') - self.consoleMessage("Download unsuccessful") - - def threadFinished(self, status): - if self.thread.task == DOWNLOAD_CFMESH: - if status: - if CfdTools.getFoamRuntime() != "MinGW": - self.consoleMessage("Download completed") - user_dir = self.thread.user_dir - self.consoleMessage("Building cfMesh. Lengthy process - please wait...") - self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, CFMESH_FILE_BASE)) - if CfdTools.getFoamRuntime() == 'WindowsDocker': - # There seem to be issues when using multi processors to build in docker - self.install_process = CfdTools.startFoamApplication( - "export WM_NCOMPPROCS=1; ./Allwmake", - "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, - 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) - else: - self.install_process = CfdTools.startFoamApplication( - "export WM_NCOMPPROCS=`nproc`; ./Allwmake", - "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, - 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) - else: - self.consoleMessage("Install completed") - # Reset foam dir for now in case the user presses 'Cancel' - CfdTools.setFoamDir(self.initial_foam_dir) - elif self.thread.task == DOWNLOAD_HISA: - if status: - if CfdTools.getFoamRuntime() != "MinGW": - self.consoleMessage("Download completed") - user_dir = self.thread.user_dir - self.consoleMessage("Building HiSA. Please wait...") - self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, HISA_FILE_BASE)) - if CfdTools.getFoamRuntime() == 'WindowsDocker': - # There seem to be issues when using multi processors to build in docker - self.install_process = CfdTools.startFoamApplication( - "export WM_NCOMPPROCS=1; ./Allwmake", - "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, - 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) - else: - self.install_process = CfdTools.startFoamApplication( - "export WM_NCOMPPROCS=`nproc`; ./Allwmake", - "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, - 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) - else: - self.consoleMessage("Install completed") - # Reset foam dir for now in case the user presses 'Cancel' - CfdTools.setFoamDir(self.initial_foam_dir) - elif self.thread.task == DOWNLOAD_DOCKER: - if status: - self.consoleMessage("Download completed") - else: - self.consoleMessage("Download unsuccessful") - self.thread = None - - def installFinished(self, exit_code): - if exit_code: - self.consoleMessage("Install finished with error {}".format(exit_code)) - else: - self.consoleMessage("Install completed") - - def downloadProgress(self, bytes_done, bytes_total): - mb_done = float(bytes_done)/(1024*1024) - msg = "Downloaded {:.2f} MB".format(mb_done) - if bytes_total > 0: - msg += " of {:.2f} MB".format(float(bytes_total)/(1024*1024)) - self.form.labelDownloadProgress.setText(msg) - - def stderrFilter(self, text): - # Print to stdout rather than stderr so as not to alarm the user - # with the spurious wmkdep errors on stderr - print(text, end='') - return '' - - def dockerCheckboxClicked(self): - if CfdTools.docker_container==None: - CfdTools.docker_container = CfdTools.DockerContainer() - CfdTools.docker_container.usedocker = self.form.cb_docker_sel.isChecked() - self.form.pb_download_install_docker.setEnabled(CfdTools.docker_container.usedocker) - self.form.pb_download_install_openfoam.setEnabled(not CfdTools.docker_container.usedocker) - self.form.pb_download_install_hisa.setEnabled(not CfdTools.docker_container.usedocker) - self.form.pb_download_install_cfMesh.setEnabled(not CfdTools.docker_container.usedocker) - self.form.gb_docker.setVisible(CfdTools.docker_container.docker_cmd!=None or CfdTools.docker_container.usedocker) - - def downloadInstallDocker(self): - # Set foam dir and output dir in preparation for using docker - CfdTools.setFoamDir(self.form.le_foam_dir.text()) - self.saveSettings() - if self.createThread(): - self.thread.task = DOWNLOAD_DOCKER - self.thread.docker_url = self.form.le_docker_url.text() - self.thread.start() - -class CfdPreferencePageSignals(QObject): - error = QtCore.Signal(str) # Signal in PySide, pyqtSignal in PyQt - finished = QtCore.Signal(bool) - status = QtCore.Signal(str) - downloadProgress = QtCore.Signal(int, int) - - -class CfdPreferencePageThread(QThread): - """ Worker thread to complete tasks in preference page """ - def __init__(self): - super(CfdPreferencePageThread, self).__init__() - self.signals = CfdPreferencePageSignals() - self.quit = False - self.user_dir = None - self.task = None - self.openfoam_url = None - self.paraview_url = None - self.cfmesh_url = None - self.hisa_url = None - self.docker_url = None - - def run(self): - self.quit = False - - try: - if self.task == DOWNLOAD_OPENFOAM: - self.downloadOpenFoam() - elif self.task == DOWNLOAD_PARAVIEW: - self.downloadParaview() - elif self.task == DOWNLOAD_CFMESH: - self.downloadCfMesh() - elif self.task == DOWNLOAD_HISA: - self.downloadHisa() - elif self.task == DOWNLOAD_DOCKER: - self.downloadDocker() - except Exception as e: - if self.quit: - self.signals.finished.emit(False) # Exit quietly since UI already destroyed - return - else: - self.signals.error.emit(str(e)) - self.signals.finished.emit(False) - return - self.signals.finished.emit(True) - - def downloadFile(self, url, **kwargs): - block_size = kwargs.get('block_size', 10*1024) - context = kwargs.get('context', None) - reporthook = kwargs.get('reporthook', None) - suffix = kwargs.get('suffix', '') - with closing(urlrequest.urlopen(url, context=context)) as response: # For Python < 3.3 backward compatibility - download_len = int(response.info().get('Content-Length', 0)) - with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp_file: - i = 0 - while True: - data = response.read(block_size) - if not data: - break - if self.quit: - raise RuntimeError("Premature termination received") - tmp_file.write(data) - i += 1 - if reporthook: - reporthook(i, block_size, download_len) - filename = tmp_file.name - return filename, response.info() - - def download(self, url, suffix, name): - self.signals.status.emit("Downloading {}, please wait...".format(name)) - try: - if hasattr(ssl, 'create_default_context'): - context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) - else: - context = None - # Download - (filename, header) = self.downloadFile( - url, suffix=suffix, reporthook=self.downloadStatus, context=context) - except Exception as ex: - raise Exception("Error downloading {}: {}".format(name, str(ex))) - - self.signals.status.emit("{} downloaded to {}".format(name, filename)) - return filename - - def downloadOpenFoam(self): - filename = self.download(self.openfoam_url, OPENFOAM_FILE_EXT, "OpenFOAM") - if QtCore.QProcess().startDetached(filename): - self.signals.status.emit("OpenFOAM installer launched - please complete the installation") - else: - raise Exception("Failed to launch OpenFOAM installer") - - def downloadParaview(self): - filename = self.download(self.paraview_url, PARAVIEW_FILE_EXT, "ParaView") - if QtCore.QProcess().startDetached(filename): - self.signals.status.emit("ParaView installer launched - please complete the installation") - else: - raise Exception("Failed to launch ParaView installer") - - def downloadCfMesh(self): - filename = self.download(self.cfmesh_url, CFMESH_FILE_EXT, "cfMesh") - - if CfdTools.getFoamRuntime() == "MinGW": - self.user_dir = None - self.signals.status.emit("Installing cfMesh...") - CfdTools.runFoamCommand( - '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. - format(CfdTools.translatePath(filename))) - else: - self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] - # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. - if CfdTools.getFoamRuntime() != 'WindowsDocker': - self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) - - self.signals.status.emit("Extracting cfMesh...") - if CfdTools.getFoamRuntime() == 'WindowsDocker': - from zipfile import ZipFile - with ZipFile(filename, 'r') as zip: - with tempfile.TemporaryDirectory() as tempdir: - zip.extractall(path=tempdir) - CfdTools.runFoamCommand( - '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' - .format(CfdTools.translatePath(os.path.join(tempdir, CFMESH_FILE_BASE)))) - else: - CfdTools.runFoamCommand( - '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. - format(CFMESH_FILE_BASE, CfdTools.translatePath(filename))) - - def downloadHisa(self): - filename = self.download(self.hisa_url, HISA_FILE_EXT, "HiSA") - - if CfdTools.getFoamRuntime() == "MinGW": - self.user_dir = None - self.signals.status.emit("Installing HiSA...") - CfdTools.runFoamCommand( - '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. - format(CfdTools.translatePath(filename))) - else: - self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] - # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. - if CfdTools.getFoamRuntime() != 'WindowsDocker': - self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) - - self.signals.status.emit("Extracting HiSA...") - if CfdTools.getFoamRuntime() == 'WindowsDocker': - from zipfile import ZipFile - with ZipFile(filename, 'r') as zip: - with tempfile.TemporaryDirectory() as tempdir: - zip.extractall(path=tempdir) - CfdTools.runFoamCommand( - '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' - .format(CfdTools.translatePath(os.path.join(tempdir, HISA_FILE_BASE)))) - else: - CfdTools.runFoamCommand( - '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. - format(HISA_FILE_BASE, CfdTools.translatePath(filename))) - - def downloadDocker(self): - self.signals.status.emit("Downloading Docker image, please wait until 'Download completed' message shown below") - if CfdTools.docker_container.container_id!=None: - CfdTools.docker_container.stop_container() - cmd = '{} pull {}'.format(CfdTools.docker_container.docker_cmd, self.docker_url) - if 'docker'in CfdTools.docker_container.docker_cmd: - cmd = cmd.replace('docker.io/','') - - CfdTools.runFoamCommand(cmd) - - def downloadStatus(self, blocks, block_size, total_size): - self.signals.downloadProgress.emit(blocks*block_size, total_size) From 33355cced8aa77ddfd8db72b1b92cb8df110ced2 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:32:40 -0600 Subject: [PATCH 04/20] Delete CfdRemotePreferencePage.ui Uploaded to wrong dir --- CfdRemotePreferencePage.ui | 856 ------------------------------------- 1 file changed, 856 deletions(-) delete mode 100644 CfdRemotePreferencePage.ui diff --git a/CfdRemotePreferencePage.ui b/CfdRemotePreferencePage.ui deleted file mode 100644 index 34ee63a7..00000000 --- a/CfdRemotePreferencePage.ui +++ /dev/null @@ -1,856 +0,0 @@ - - - CfdRemotePreferencePage - - - - 0 - 0 - 488 - 1531 - - - - Remote Processing - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - - 12 - 75 - true - - - - - - - 1 - - - Use Remote Processing - - - - - - - - - - 1 - - - About Remote Processing - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - - 0 - 0 - - - - false - - - - - - - - 0 - 0 - - - - Add Profile - - - - - - - - 0 - 0 - - - - Delete Profile - - - - - - - - 75 - true - - - - Host profile: - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - - 0 - 0 - - - - - 75 - true - - - - Username on remote host: - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Remote hostname or IP address: - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - Ping Remote Host - - - - - - - Test SSH - - - - - - - - - 8 - - - 8 - - - 8 - - - 0 - - - - - - 75 - true - - - - Meshing - - - Qt::AlignCenter - - - - - - - - 75 - true - - - - OpenFOAM - - - Qt::AlignCenter - - - - - - - - - 8 - - - 0 - - - 8 - - - 8 - - - - - Processes - - - - - - - Threads - - - - - - - - 0 - 0 - - - - - - - - Processes - - - - - - - Threads - - - - - - - - - - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - 14 - - - 6 - - - - - - 75 - true - - - - Remote output path - - - - - - - - - - true - - - The directory to which case folders are written. Used unless overridden on a per-analysis basis. - - - false - - - - - - - - 75 - true - - - - Remote OpenFOAM executable directory - - - - - - - ... - - - - - - - true - - - The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. - - - false - - - - - - - true - - - The full path of the ParaView executable. Leave blank to use search path. - - - false - - - - - - - - 75 - true - - - - Remote gmsh executable path - - - - - - - true - - - ... - - - - - - - true - - - ... - - - - - - - true - - - ... - - - - - - - - 75 - true - - - - Remote ParaView executable path - - - - - - - Add filename to output path ? - - - - - - - - - 8 - - - 8 - - - 8 - - - 6 - - - 14 - - - 8 - - - - - 0 - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 75 - true - - - - Remote Software dependencies - - - - - - - true - - - Run remote host dependency checker - - - - - - - - - - OpenFOAM - - - - - - - 0 - 0 - - - - Choose existing file ... - - - - - - - Install OpenFOAM - - - - - - - - 0 - 0 - - - - URL: - - - - - - - - - - - - - ParaView - - - - - - - 0 - 0 - - - - Choose existing file ... - - - - - - - Install ParaView - - - - - - - - 0 - 0 - - - - URL: - - - - - - - - - - - - - cfMesh - - - - - - - 0 - 0 - - - - URL: - - - - - - - Install cfMesh on remote host - - - - - - - - 0 - 0 - - - - Choose existing file ... - - - - - - - - - - - - - HiSA - - - - - - Install HiSA - - - - - - - - 0 - 0 - - - - URL: - - - - - - - - 0 - 0 - - - - Choose existing file ... - - - - - - - - - - - - - Docker Container - - - - - - Use docker: - - - - - - - - - - Download from URL: - - - - - - - - - - Install Docker Container - - - - - - - - - - - - 8 - - - 5 - - - 8 - - - 8 - - - - - - 75 - true - - - - Output - - - - - - - - 0 - 0 - - - - - 0 - 120 - - - - QTextEdit::NoWrap - - - - - - - - - le_foam_dir - tb_choose_remote_foam_dir - le_paraview_path - tb_choose_paraview_path - le_gmsh_path - tb_choose_remote_gmsh_path - le_output_path - tb_choose_remote_output_dir - pb_run_dependency_checker - pb_download_install_openfoam - le_openfoam_url - tb_pick_openfoam_file - le_paraview_url - pb_download_install_paraview - tb_pick_paraview_file - le_cfmesh_url - pb_download_install_cfMesh - tb_pick_cfmesh_file - le_hisa_url - pb_download_install_hisa - tb_pick_hisa_file - - - - From ca16afdf2426bb9976545ca1ee796c93c63cfe88 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:32:55 -0600 Subject: [PATCH 05/20] Delete TaskPanelCfdMesh.py Uploaded to wrong dir --- TaskPanelCfdMesh.py | 710 -------------------------------------------- 1 file changed, 710 deletions(-) delete mode 100644 TaskPanelCfdMesh.py diff --git a/TaskPanelCfdMesh.py b/TaskPanelCfdMesh.py deleted file mode 100644 index 6df3add3..00000000 --- a/TaskPanelCfdMesh.py +++ /dev/null @@ -1,710 +0,0 @@ -# *************************************************************************** -# * * -# * Copyright (c) 2016 - Bernd Hahnebach * -# * Copyright (c) 2017 Alfred Bogaers (CSIR) * -# * Copyright (c) 2017 Johan Heyns (CSIR) * -# * Copyright (c) 2017 Oliver Oxtoby (CSIR) * -# * Copyright (c) 2019-2022 Oliver Oxtoby * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License as * -# * published by the Free Software Foundation, either version 3 of the * -# * License, or (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Lesser General Public * -# * License along with this program. If not, * -# * see . * -# * * -# *************************************************************************** -# -# LinuxGuy123@gmail.com's notes: -# -# -# -# TODOs, in addition to TODOs in the code itself -# -# - check that the appropriate controls are enabled for the host. Most specifically, -# Edit case shouldn't be enabled for remote hosts. Paraview doesn't work on remote hosts either. -# Also check mesh, etc. -# -# - right now there is no way to edit the case on a remote host. This could be enabled by -# copying back the case to the local machine, allowing the user to edit the files in a temp dir -# and then copying them back to the remote host -# -# - the host name was passed into the meshing routines via the global variable. Really the hostname -# should be passed into the meshing routines via the mesh object -# -#- add use filename extension to the output path. For both local and remote useRemoteProcessing -# -#- copy the mesh back to the local computer for Paraview, Load surface mesh and Check Mesh. - - - - -from __future__ import print_function -import FreeCAD -import os -import os.path -from CfdOF.Mesh import CfdMesh -import time -from datetime import timedelta -from CfdOF import CfdTools -from CfdOF.CfdTools import setQuantity, getQuantity, storeIfChanged -from CfdOF.Mesh import CfdMeshTools -from CfdOF.CfdConsoleProcess import CfdConsoleProcess -if FreeCAD.GuiUp: - import FreeCADGui - from PySide import QtCore - from PySide import QtCore - from PySide import QtGui - from PySide.QtCore import Qt - from PySide.QtGui import QApplication - - -class TaskPanelCfdMesh: - """ The TaskPanel for editing References property of CfdMesh objects and creation of new CFD mesh """ - def __init__(self, obj): - self.mesh_obj = obj - self.analysis_obj = CfdTools.getParentAnalysisObject(self.mesh_obj) - self.form = FreeCADGui.PySideUic.loadUi(os.path.join(CfdTools.getModulePath(), 'Gui', "TaskPanelCfdMesh.ui")) - - self.console_message_cart = '' - self.error_message = '' - self.mesh_obj.Proxy.cart_mesh = CfdMeshTools.CfdMeshTools(self.mesh_obj) - self.paraviewScriptName = "" - - self.mesh_obj.Proxy.mesh_process = CfdConsoleProcess(finished_hook=self.meshFinished, - stdout_hook=self.gotOutputLines, - stderr_hook=self.gotErrorLines) - - #set the prefs and host prefs locations - self.prefs_location = CfdTools.getPreferencesLocation() - self.host_prefs_location = self.prefs_location + "/Hosts" - self.useRemoteProcessing = FreeCAD.ParamGet(self.prefs_location).GetBool('UseRemoteProcessing', 0) - - #setting these here so they get created as globals - #they also get initiated in loadProfile() - # self.use_remote_processing = False <- this is set above before the control is loaded - - self.profile_name = "" - self.hostname = "" - self.username = "" - self.mesh_processes = 0 - self.mesh_threads = 0 - self.foam_processes = 0 - self.foam_threads = 0 - self.foam_dir = "" - self.output_path = "" - self.gmsh_path = "" - self.add_filename_to_output = False - - #add a local host to cb_profile - self.form.cb_profile.addItem("local") - - # if using remote processing, add the host profiles as well - if self.useRemoteProcessing: - self.loadProfileNames() - else: - #disable cb_profile so that users aren't trying to change the host - self.form.cb_profile.setEnabled(False) - - # load the local profile - self.loadProfile("local") - - self.Timer = QtCore.QTimer() - self.Timer.setInterval(1000) - self.Timer.timeout.connect(self.update_timer_text) - - # set up the profiles combo box connection - self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) - - self.form.cb_utility.activated.connect(self.choose_utility) - - self.form.pb_write_mesh.clicked.connect(self.writeMesh) - - self.form.pb_edit_mesh.clicked.connect(self.editMesh) - - self.form.pb_run_mesh.clicked.connect(self.runMesh) - - self.form.pb_stop_mesh.clicked.connect(self.killMeshProcess) - self.form.pb_paraview.clicked.connect(self.openParaview) - self.form.pb_load_mesh.clicked.connect(self.pbLoadMeshClicked) - self.form.pb_clear_mesh.clicked.connect(self.pbClearMeshClicked) - self.form.pb_searchPointInMesh.clicked.connect(self.searchPointInMesh) - self.form.pb_check_mesh.clicked.connect(self.checkMeshClicked) - - self.radioGroup = QtGui.QButtonGroup() - self.radioGroup.addButton(self.form.radio_explicit_edge_detection) - self.radioGroup.addButton(self.form.radio_implicit_edge_detection) - - self.form.snappySpecificProperties.setVisible(False) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) - - self.form.cb_utility.addItems(CfdMesh.MESHER_DESCRIPTIONS) - - self.form.if_max.setToolTip("Enter 0 to use default value") - self.form.pb_searchPointInMesh.setToolTip("Specify below a point vector inside of the mesh or press 'Search' " - "to try to automatically find a point") - self.form.if_cellsbetweenlevels.setToolTip("Number of cells between each of level of refinement") - self.form.if_edgerefine.setToolTip("Number of refinement levels for all edges") - self.form.radio_explicit_edge_detection.setToolTip("Find surface edges using explicit (eMesh) detection") - self.form.radio_implicit_edge_detection.setToolTip("Find surface edges using implicit detection") - - self.load() - self.updateUI() - - self.Start = time.time() - self.Timer.start() - - # loads the profiles names into the profile combo box - def loadProfileNames(self): - profileDir = self.prefs_location + "/Hosts" - profiles = FreeCAD.ParamGet(profileDir) - profileList = profiles.GetGroups() - for item in profileList: - self.form.cb_profile.addItem(item) - - - # load profile parameters into the controls and local vars - def loadProfile(self, profile_name): - - #set the global profile name - self.profile_name = profile_name - - #set the global host prefs location - self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name - - #set the other global vars - if profile_name == "": - print("Error: no host profile selected") - return - - # set the vars to the local parameters - if profile_name == "local": - self.hostname = "local" - # the local code doesn't use these vars, so don't set them - # dangerous. - """ - self.username = "" - self.mesh_processes = 0 - self.mesh_threads = 0 - self.foam_processes = 0 - self.foam_threads = 0 - self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") - self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") - self.output_path = "" - self.add_filename_to_output = False - """ - else: - #set the vars to the remote host parameters - # most of these aren't used, at least not in this page - hostPrefs = self.host_prefs_location - self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") - self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") - self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") - self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") - self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") - self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") - self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") - self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") - self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") - - #now set the control values - self.mesh_obj.NumberOfProcesses = self.mesh_processes - self.mesh_obj.NumberOfThreads = self.mesh_threads - - #TODO: fix these, if we need to. - #self.form.le_mesh_processes.setText(str(self.mesh_processes)) - #self.form.le_mesh_threads.setText(str(self.mesh_threads)) - - #self.form.le_hostname.setText(self.hostname) - #self.form.le_username.setText(self.username) - - #self.form.le_foam_processes.setText(str(self.foam_processes)) - #self.form.le_foam_threads.setText(str(self.foam_threads)) - #self.form.le_foam_dir.setText(self.foam_dir) - #self.form.le_output_path.setText(self.output_path) - #self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) - - - # this gets called when the user changes the profile - def profileChanged(self): - print("The profile was changed") - # change the global profile name - self.profile_name = self.form.cb_profile.currentText() - #load the values for the new profile - print ("New profile is ", self.profile_name) - self.loadProfile(self.profile_name) - # TODO enable and disable the appropriate controls here - # Remote hosts can't edit the case nor Paraview, check mesh, etc. - # Nor load surface mesh nor clear surface mesh. - - - # test routine to run a mesh without - # a proxy. The real routine is runMesh way below - def runRemoteMesh(self): - # run remote meshing directly, without a proxy - #profile_prefs = CfdTools.getPreferencesLocation() + '/Hosts/' + self.profile_name - remote_user = self.username - remote_hostname = self.hostname - - # create the ssh connection command - ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' ' - - # Get the working directory for the mesh - working_dir = self.output_path - #TODO: add filename to the path if selected - - # create the command to do the actual work - command = 'EOT \n' - command += 'cd ' + working_dir + '/meshCase \n' - command += './Allmesh \n' - command += 'exit \n' - command += 'EOT' - command = ssh_prefix + ' << ' + command - - self.consoleMessage("Starting remote meshing...") - try: - CfdTools.runFoamCommand(command) - print("Remote meshing is complete.") - self.consoleMessage("Remote meshing is complete.") - except Exception as error: - self.consoleMessage("Error meshing on remote host: " + str(error)) - - def getStandardButtons(self): - return int(QtGui.QDialogButtonBox.Close) - # def reject() is called on close button - - def reject(self): - FreeCADGui.ActiveDocument.resetEdit() - return True - - def closed(self): - # We call this from unsetEdit to ensure cleanup - self.store() - self.mesh_obj.Proxy.mesh_process.terminate() - self.mesh_obj.Proxy.mesh_process.waitForFinished() - self.Timer.stop() - FreeCAD.ActiveDocument.recompute() - - def load(self): - """ Fills the widgets """ - setQuantity(self.form.if_max, self.mesh_obj.CharacteristicLengthMax) - point_in_mesh = self.mesh_obj.PointInMesh.copy() - setQuantity(self.form.if_pointInMeshX, point_in_mesh.get('x')) - setQuantity(self.form.if_pointInMeshY, point_in_mesh.get('y')) - setQuantity(self.form.if_pointInMeshZ, point_in_mesh.get('z')) - - self.form.if_cellsbetweenlevels.setValue(self.mesh_obj.CellsBetweenLevels) - self.form.if_edgerefine.setValue(self.mesh_obj.EdgeRefinement) - self.form.radio_implicit_edge_detection.setChecked(self.mesh_obj.ImplicitEdgeDetection) - self.form.radio_explicit_edge_detection.setChecked(not self.mesh_obj.ImplicitEdgeDetection) - - index_utility = CfdTools.indexOrDefault(list(zip( - CfdMesh.MESHERS, CfdMesh.DIMENSION, CfdMesh.DUAL_CONVERSION)), - (self.mesh_obj.MeshUtility, self.mesh_obj.ElementDimension, self.mesh_obj.ConvertToDualMesh), 0) - self.form.cb_utility.setCurrentIndex(index_utility) - - def updateUI(self): - case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir - self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) - self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) - self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) - self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) - self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) - - utility = CfdMesh.MESHERS[self.form.cb_utility.currentIndex()] - if utility == "snappyHexMesh": - self.form.snappySpecificProperties.setVisible(True) - else: - self.form.snappySpecificProperties.setVisible(False) - - def store(self): - mesher_idx = self.form.cb_utility.currentIndex() - storeIfChanged(self.mesh_obj, 'CharacteristicLengthMax', getQuantity(self.form.if_max)) - storeIfChanged(self.mesh_obj, 'MeshUtility', CfdMesh.MESHERS[mesher_idx]) - storeIfChanged(self.mesh_obj, 'ElementDimension', CfdMesh.DIMENSION[mesher_idx]) - storeIfChanged(self.mesh_obj, 'CellsBetweenLevels', self.form.if_cellsbetweenlevels.value()) - storeIfChanged(self.mesh_obj, 'EdgeRefinement', self.form.if_edgerefine.value()) - storeIfChanged(self.mesh_obj, 'ConvertToDualMesh', CfdMesh.DUAL_CONVERSION[mesher_idx]) - storeIfChanged(self.mesh_obj, 'ImplicitEdgeDetection', self.form.radio_implicit_edge_detection.isChecked()) - - point_in_mesh = {'x': getQuantity(self.form.if_pointInMeshX), - 'y': getQuantity(self.form.if_pointInMeshY), - 'z': getQuantity(self.form.if_pointInMeshZ)} - - if self.mesh_obj.MeshUtility == 'snappyHexMesh': - storeIfChanged(self.mesh_obj, 'PointInMesh', point_in_mesh) - - self.mesh_obj.Proxy.cart_mesh = CfdMeshTools.CfdMeshTools(self.mesh_obj) - - def consoleMessage(self, message="", colour_type=None, timed=True): - if timed: - self.console_message_cart += \ - '{:4.1f}: '.format(CfdTools.getColour('Logging'), time.time() - self.Start) - if colour_type: - self.console_message_cart += \ - '{}
'.format(CfdTools.getColour(colour_type), message) - else: - self.console_message_cart += message + '
' - self.form.te_output.setText(self.console_message_cart) - self.form.te_output.moveCursor(QtGui.QTextCursor.End) - if FreeCAD.GuiUp: - FreeCAD.Gui.updateGui() - - def update_timer_text(self): - if self.mesh_obj.Proxy.mesh_process.state() == QtCore.QProcess.ProcessState.Running: - self.form.l_time.setText('Time: ' + CfdTools.formatTimer(time.time() - self.Start)) - - def choose_utility(self, index): - if index < 0: - return - utility = CfdMesh.MESHERS[self.form.cb_utility.currentIndex()] - if utility == "snappyHexMesh": - self.form.snappySpecificProperties.setVisible(True) - else: - self.form.snappySpecificProperties.setVisible(False) - - def writeMesh(self): - import importlib - importlib.reload(CfdMeshTools) - self.console_message_cart = '' - self.Start = time.time() - # Re-initialise CfdMeshTools with new parameters - self.store() - - #get the host name we are writing the mesh case for - host_profile = self.profile_name - print ("Writing mesh for host profile " + host_profile + ".") - - FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") - FreeCADGui.doCommand("from CfdOF import CfdTools") - FreeCADGui.doCommand("cart_mesh = " - "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") - FreeCADGui.doCommand("FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy.cart_mesh = cart_mesh") - cart_mesh = self.mesh_obj.Proxy.cart_mesh - cart_mesh.progressCallback = self.progressCallback - - # Start writing the mesh files - if host_profile == "local": - self.consoleMessage("Preparing local mesh ...") - else: - self.consoleMessage("Preparing remote mesh for " + host_profile + "...") - try: - QApplication.setOverrideCursor(Qt.WaitCursor) - setQuantity(self.form.if_max, str(cart_mesh.getClmax())) - # Re-update the data in case ClMax was auto-set to avoid spurious update detection on next write - self.store() - print('Part to mesh:\n Name: ' - + cart_mesh.part_obj.Name + ', Label: ' - + cart_mesh.part_obj.Label + ', ShapeType: ' - + cart_mesh.part_obj.Shape.ShapeType) - print(' CharacteristicLengthMax: ' + str(cart_mesh.clmax)) - - if host_profile == "local": - FreeCADGui.doCommand("cart_mesh.writeMesh('local')") - else: - FreeCADGui.doCommand("cart_mesh.writeMesh('"+ host_profile +"')") - - except Exception as ex: - self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') - raise - else: - self.analysis_obj.NeedsMeshRerun = True - finally: - QApplication.restoreOverrideCursor() - - # Update the UI - self.updateUI() - - - def progressCallback(self, message): - self.consoleMessage(message) - - def checkMeshClicked(self): - if CfdTools.getFoamRuntime() == "PosixDocker": - CfdTools.startDocker() - self.Start = time.time() - try: - QApplication.setOverrideCursor(Qt.WaitCursor) - FreeCADGui.doCommand("from CfdOF import CfdTools") - FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") - FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") - FreeCADGui.doCommand("cart_mesh = " - "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") - FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") - FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") - FreeCADGui.doCommand("cart_mesh.error = False") - FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('checkMesh -meshQuality', cart_mesh.meshCaseDir)") - FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") - FreeCADGui.doCommand("proxy.running_from_macro = True") - self.mesh_obj.Proxy.running_from_macro = False - FreeCADGui.doCommand("if proxy.running_from_macro:\n" + - " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + - " mesh_process.start(cmd, env_vars=env_vars)\n" + - " mesh_process.waitForFinished()\n" + - "else:\n" + - " proxy.mesh_process.start(cmd, env_vars=env_vars)") - if self.mesh_obj.Proxy.mesh_process.waitForStarted(): - self.form.pb_check_mesh.setEnabled(False) # Prevent user running a second instance - self.form.pb_run_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(False) - #self.form.pb_write_remote_mesh.setEnabled(False) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) - self.form.pb_load_mesh.setEnabled(False) - self.consoleMessage("Mesh check started ...") - else: - self.consoleMessage("Error starting mesh check process", 'Error') - self.mesh_obj.Proxy.cart_mesh.error = True - - except Exception as ex: - self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') - finally: - QApplication.restoreOverrideCursor() - - - def editMesh(self): - case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir - self.consoleMessage("Please edit the case input files externally at: {}\n".format(case_path)) - CfdTools.openFileManager(case_path) - - - #TODO: won't run remotely with a proxy yet. Fix this. - # Presently running without a proxy in runRemoteMesh way above. - def runMesh(self): - if CfdTools.getFoamRuntime() == "PosixDocker": - CfdTools.startDocker() - - self.Start = time.time() - - # Check for changes that require mesh re-write - self.store() - if self.analysis_obj.NeedsMeshRewrite: - if FreeCAD.GuiUp: - if QtGui.QMessageBox.question( - None, - "CfdOF Workbench", - "The case setup for the mesher may need to be re-written based on changes you have made to the " - "model.\n\nWrite mesh case first?", defaultButton=QtGui.QMessageBox.Yes - ) == QtGui.QMessageBox.Yes: - self.Start = time.time() - self.writeMesh() - else: - self.Start = time.time() - - try: - QApplication.setOverrideCursor(Qt.WaitCursor) - - self.consoleMessage("Initializing {} ...".format(self.mesh_obj.MeshUtility)) - FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") - FreeCADGui.doCommand("from CfdOF import CfdTools") - FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") - FreeCADGui.doCommand("from FreeCAD import ParamGet") - - FreeCADGui.doCommand("cart_mesh = " + - "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") - - FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") - FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") - FreeCADGui.doCommand("cart_mesh.error = False") - - # run locally - if self.profile_name == "local": - FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False)") - FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") - - FreeCADGui.doCommand("print('cmd:')") - FreeCADGui.doCommand("print(cmd)") - #FreeCADGui.doCommand("print('env_vars:' + env_vars)") - - FreeCADGui.doCommand("proxy.running_from_macro = True") - self.mesh_obj.Proxy.running_from_macro = False - - FreeCADGui.doCommand("if proxy.running_from_macro:\n" + - " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + - " mesh_process.start(cmd, env_vars=env_vars)\n" + - " mesh_process.waitForFinished()\n" + - "else:\n" + - " proxy.mesh_process.start(cmd, env_vars=env_vars)") - - # run on remote host - else: - #self.runRemoteMesh() #For testing the non proxy function above - - # Get the username and hostname for the remote host - prefsCmd = "profile_prefs = CfdTools.getPreferencesLocation() + " + '"/Hosts/' + self.profile_name + '"' - #print("prefsCmd:" + prefsCmd) - FreeCADGui.doCommand(prefsCmd) - FreeCADGui.doCommand("print('profile_prefs:' + profile_prefs)") - - FreeCADGui.doCommand("remote_user = FreeCAD.ParamGet(profile_prefs).GetString('Username', '')") - FreeCADGui.doCommand("remote_hostname =FreeCAD.ParamGet(profile_prefs).GetString('Hostname', '')") - - #FreeCADGui.doCommand("print('username:' + remote_user)") - #FreeCADGui.doCommand("print('hostname:' + remote_hostname)") - - # create the ssh connection command - FreeCADGui.doCommand("ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' '") - - # Get the working directory for the mesh - FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") - #FreeCADGui.doCommand("print('working directory:' + working_dir)") - - - # create the command to do the actual work - FreeCADGui.doCommand("command = 'EOT \\n' \n" + - "command += 'cd ' + working_dir + '/meshCase \\n' \n" + - "command += './Allmesh \\n' \n" + - "command += 'exit \\n' \n" + - "command += 'EOT' \n") - FreeCADGui.doCommand("command = ssh_prefix + ' << ' + command + '\\n'") - - #FreeCADGui.doCommand("print(command)") - - FreeCADGui.doCommand("runCommand = CfdTools.makeRunCommand(command, None)") - - FreeCADGui.doCommand("proxy.running_from_macro = True") - self.mesh_obj.Proxy.running_from_macro = False - - FreeCADGui.doCommand("if proxy.running_from_macro:\n" + - " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + - " mesh_process.start(runCommand)\n" + - " mesh_process.waitForFinished()\n" + - "else:\n" + - " proxy.mesh_process.start(runCommand)") - - time.sleep(2) - if self.mesh_obj.Proxy.mesh_process.waitForStarted(): - # enable/disable the correct buttons - """ - if self.profile_name == "local": - self.form.pb_stop_mesh.setEnabled(True) - - self.form.pb_run_mesh.setEnabled(False) - #self.form.pb_run_remote_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(False) - #self.form.pb_write_remote_mesh.setEnabled(False) - self.form.pb_edit_mesh.setEnabled(False) - #self.form.pb_edit_remote_mesh.setEnabled(False) - - self.form.pb_check_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) - self.form.pb_load_mesh.setEnabled(False) - else: - if self.useRemoteProcessing: - #self.form.pb_stop_remote_mesh.setEnabled(True) - - self.form.pb_run_mesh.setEnabled(False) - #self.form.pb_run_remote_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(False) - self.form.pb_write_remote_mesh.setEnabled(False) - self.form.pb_edit_mesh.setEnabled(False) - self.form.pb_edit_remote_mesh.setEnabled(False) - - self.form.pb_check_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) - self.form.pb_load_mesh.setEnabled(False) - """ - self.consoleMessage("Mesher started ...") - else: - self.consoleMessage("Error starting meshing process", 'Error') - self.mesh_obj.Proxy.cart_mesh.error = True - except Exception as ex: - self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') - raise - finally: - QApplication.restoreOverrideCursor() - - - def killMeshProcess(self): - self.consoleMessage("Meshing manually stopped") - self.error_message = 'Meshing interrupted' - self.mesh_obj.Proxy.mesh_process.terminate() - # Note: meshFinished will still be called - - def gotOutputLines(self, lines): - pass - - def gotErrorLines(self, lines): - print_err = self.mesh_obj.Proxy.mesh_process.processErrorOutput(lines) - if print_err is not None: - self.consoleMessage(print_err, 'Error') - - def meshFinished(self, exit_code): - if exit_code == 0: - self.consoleMessage('Meshing completed') - self.analysis_obj.NeedsMeshRerun = False - self.form.pb_run_mesh.setEnabled(True) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(True) - self.form.pb_write_mesh.setEnabled(True) - self.form.pb_check_mesh.setEnabled(True) - self.form.pb_load_mesh.setEnabled(True) - - if self.useRemoteProcessing: - pass - #self.form.pb_run_remote_mesh.setEnabled(True) - #self.form.pb_stop_remote_mesh.setEnabled(False) - #self.form.pb_write_remote_mesh.setEnabled(True) - - else: - self.consoleMessage("Meshing exited with error", 'Error') - self.form.pb_run_mesh.setEnabled(True) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(True) - self.form.pb_check_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) - - if self.useRemoteProcessing: - pass - #self.form.pb_run_remote_mesh.setEnabled(True) - #self.form.pb_stop_remote_mesh.setEnabled(False) - #self.form.pb_write_remote_mesh.setEnabled(True) - - self.error_message = '' - # Get rid of any existing loaded mesh - self.pbClearMeshClicked() - self.updateUI() - - def openParaview(self): - QApplication.setOverrideCursor(Qt.WaitCursor) - case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) - script_name = "pvScriptMesh.py" - try: - CfdTools.startParaview(case_path, script_name, self.consoleMessage) - finally: - QApplication.restoreOverrideCursor() - - def pbLoadMeshClicked(self): - self.consoleMessage("Reading mesh ...", timed=False) - prev_write_mesh = self.analysis_obj.NeedsMeshRewrite - self.mesh_obj.Proxy.cart_mesh.loadSurfMesh() - self.analysis_obj.NeedsMeshRewrite = prev_write_mesh - self.consoleMessage('Triangulated representation of the surface mesh is shown - ', timed=False) - self.consoleMessage("Please view in Paraview for accurate display.\n", timed=False) - - def pbClearMeshClicked(self): - prev_write_mesh = self.analysis_obj.NeedsMeshRewrite - for m in self.mesh_obj.Group: - if m.isDerivedFrom("Fem::FemMeshObject"): - FreeCAD.ActiveDocument.removeObject(m.Name) - self.analysis_obj.NeedsMeshRewrite = prev_write_mesh - FreeCAD.ActiveDocument.recompute() - - def searchPointInMesh(self): - print ("Searching for an internal vector point ...") - # Apply latest mesh size - self.store() - pointCheck = self.mesh_obj.Proxy.cart_mesh.automaticInsidePointDetect() - if pointCheck is not None: - iMPx, iMPy, iMPz = pointCheck - setQuantity(self.form.if_pointInMeshX, str(iMPx) + "mm") - setQuantity(self.form.if_pointInMeshY, str(iMPy) + "mm") - setQuantity(self.form.if_pointInMeshZ, str(iMPz) + "mm") From d2fc68c4aa8feda8ccc8898992b0a383af3a5b40 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:33:11 -0600 Subject: [PATCH 06/20] Delete TaskPanelCfdMesh.ui Uploaded to wrong dir --- TaskPanelCfdMesh.ui | 611 -------------------------------------------- 1 file changed, 611 deletions(-) delete mode 100644 TaskPanelCfdMesh.ui diff --git a/TaskPanelCfdMesh.ui b/TaskPanelCfdMesh.ui deleted file mode 100644 index fd907248..00000000 --- a/TaskPanelCfdMesh.ui +++ /dev/null @@ -1,611 +0,0 @@ - - - CfdMesh - - - - 0 - 0 - 512 - 1127 - - - - CFD Mesh - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - 14 - - - 8 - - - - - - 75 - true - - - - Mesh Parameters - - - - - - - Base element size: - - - - - - - - - - - - - - - 0 - 0 - - - - - - - 0.0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1.000000000000000 - - - 1000000000.000000000000000 - - - mm - - - g - - - - - - - Mesh utility: - - - - - - - - 75 - true - - - - Mesh Parameters - - - - - - - - - - 0 - 0 - - - - QFrame::Panel - - - QFrame::Plain - - - 0 - - - - - - Search - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 5 - - - - - - - - - 50 - false - - - - Point in mesh - - - - - - - - 0 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - 0 - - - - 0 - - - 0 - - - - - 3 - - - 0.001000000000000 - - - 1.000000000000000 - - - 0.050000000000000 - - - - - - - No of cells between levels - - - - - - - Relative edge refinement - - - - - - - Edge detection - - - - - - - 1 - - - 100 - - - - - - - Implicit - - - - - - - Explicit - - - - - - - - - - - 0 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 2 - - - - - - 0 - 0 - - - - - - - 0.0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1.000000000000000 - - - 1000000000.000000000000000 - - - mm - - - g - - - - - - - - 0 - 0 - - - - - - - 0.0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1.000000000000000 - - - 1000000000.000000000000000 - - - mm - - - g - - - - - - - - 0 - 0 - - - - - - - 0.0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1.000000000000000 - - - 1000000000.000000000000000 - - - mm - - - g - - - - - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - - 0 - 0 - - - - - 75 - true - - - - Host - - - - - - - - 0 - 0 - - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - 8 - - - - - Stop - - - - - - - Run mesh case - - - - - - - Edit - - - - - - - Write mesh case - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - 14 - - - 8 - - - - - Clear surface mesh - - - - - - - Load surface mesh - - - - - - - - 75 - true - - - - Visualisation - - - - - - - Paraview - - - - - - - Check Mesh - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - 8 - - - - - Status - - - - - - - - - - 10 - - - - - - - - - - - QTextEdit::NoWrap - - - - - - - - - - - - Gui::InputField - QLineEdit -
Gui/InputField.h
-
-
- - pb_run_mesh - pb_stop_mesh - pb_paraview - pb_load_mesh - pb_clear_mesh - cb_utility - if_max - pb_searchPointInMesh - if_pointInMeshX - if_pointInMeshY - if_pointInMeshZ - te_output - - - -
From 2a9f26324c22fffa8f1a8313c6d2272f06458908 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:33:28 -0600 Subject: [PATCH 07/20] Delete TaskPanelCfdSolverControl.ui Uploaded to wrong dir --- TaskPanelCfdSolverControl.ui | 278 ----------------------------------- 1 file changed, 278 deletions(-) delete mode 100644 TaskPanelCfdSolverControl.ui diff --git a/TaskPanelCfdSolverControl.ui b/TaskPanelCfdSolverControl.ui deleted file mode 100644 index 4543ded8..00000000 --- a/TaskPanelCfdSolverControl.ui +++ /dev/null @@ -1,278 +0,0 @@ - - - AnalysisControl - - - - 0 - 0 - 612 - 1067 - - - - Analysis control - - - - - - - 75 - true - - - - OpenFOAM Solver - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - 14 - - - 6 - - - - - - 0 - 0 - - - - - 75 - true - - - - Host - - - - - - - - 0 - 0 - - - - false - - - - - - -1 - - - - - - - - - 8 - - - 8 - - - 8 - - - 6 - - - 14 - - - 8 - - - - - true - - - Stop - - - - - - - true - - - Run OF case - - - - - - - true - - - Edit - - - - - - - Write OF case - - - - - - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - 8 - - - 8 - - - 8 - - - 6 - - - 14 - - - 8 - - - - - - 75 - true - - - - Results - - - - - - - - 75 - true - - - - - - - - - - - true - - - Paraview - - - - - - - - - Status - - - - - - - - 0 - 120 - - - - QTextEdit::NoWrap - - - - - - - - - 8 - - - 6 - - - 8 - - - 6 - - - 8 - - - - - - 10 - - - - - - - - - - - - - - From 2b67db0aab52d0005a8253944b109ddded72781b Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 14:59:49 -0600 Subject: [PATCH 08/20] Delete CfdTools.py Uploaded to wrong dir --- CfdTools.py | 2043 --------------------------------------------------- 1 file changed, 2043 deletions(-) delete mode 100644 CfdTools.py diff --git a/CfdTools.py b/CfdTools.py deleted file mode 100644 index ff5cbcd9..00000000 --- a/CfdTools.py +++ /dev/null @@ -1,2043 +0,0 @@ -# *************************************************************************** -# * * -# * Copyright (c) 2015 - Qingfeng Xia * -# * Copyright (c) 2017 Johan Heyns (CSIR) * -# * Copyright (c) 2017 Oliver Oxtoby (CSIR) * -# * Copyright (c) 2017 Alfred Bogaers (CSIR) * -# * Copyright (c) 2019-2022 Oliver Oxtoby * -# * Copyright (c) 2022 Jonathan Bergh * -# * * -# * This program is free software: you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License as * -# * published by the Free Software Foundation, either version 3 of the * -# * License, or (at your option) any later version. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Lesser General Public License for more details. * -# * * -# * You should have received a copy of the GNU Lesser General Public * -# * License along with this program. If not, * -# * see . * -# * * -# *************************************************************************** - -# Utility functions like mesh exporting, shared by any CFD solver - -from __future__ import print_function - -import os -import os.path -import glob -import shutil -import tempfile -import numbers -import platform -import subprocess -import sys -import math -from datetime import timedelta -import FreeCAD -from FreeCAD import Units -import Part -import BOPTools -from BOPTools import SplitFeatures -from CfdOF.CfdConsoleProcess import CfdConsoleProcess -from CfdOF.CfdConsoleProcess import removeAppimageEnvironment -from PySide import QtCore - -# added for runCommand -from PySide.QtCore import QProcess # I added -from PySide.QtGui import QApplication # I added - -if FreeCAD.GuiUp: - import FreeCADGui - from PySide import QtGui - from PySide.QtGui import QFormLayout, QGridLayout - - -# Some standard install locations that are searched if an install directory is not specified -# Supports variable expansion and Unix-style globs (in which case the last lexically-sorted match will be used) -FOAM_DIR_DEFAULTS = {'Windows': ['C:\\Program Files\\ESI-OpenCFD\\OpenFOAM\\v*', - '~\\AppData\\Roaming\\ESI-OpenCFD\\OpenFOAM\\v*', - 'C:\\Program Files\\blueCFD-Core-*\\OpenFOAM-*'], - 'Linux': ['/usr/lib/openfoam/openfoam*', # ESI official packages - '/opt/openfoam*', '/opt/openfoam-dev', # Foundation official packages - '~/openfoam/OpenFOAM-v*', - '~/OpenFOAM/OpenFOAM-*.*', '~/OpenFOAM/OpenFOAM-dev'], # Typical self-built locations - "Darwin": ['~/OpenFOAM/OpenFOAM-*.*', '~/OpenFOAM/OpenFOAM-dev'] - } - -PARAVIEW_PATH_DEFAULTS = { - "Windows": ["C:\\Program Files\\ParaView *\\bin\\paraview.exe"], - "Linux": [], - "Darwin": [] - } - -QUANTITY_PROPERTIES = ['App::PropertyQuantity', - 'App::PropertyLength', - 'App::PropertyDistance', - 'App::PropertyAngle', - 'App::PropertyArea', - 'App::PropertyVolume', - 'App::PropertySpeed', - 'App::PropertyAcceleration', - 'App::PropertyForce', - 'App::PropertyPressure'] - -docker_container = None - -def getDefaultOutputPath(): - prefs = getPreferencesLocation() - output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") - if not output_path: - output_path = tempfile.gettempdir() - output_path = os.path.normpath(output_path) - return output_path - -def getDefaultRemoteOutputPath(): - prefs = getPreferencesLocation() - output_path = FreeCAD.ParamGet(prefs).GetString("DefaultRemoteOutputPath", "") - if not output_path: - #TODO: fix this so it will run on the server - #should be /home/username from the reference page - # output_path = tempfile.gettempdir() - output_path = "" - # assume the remote computer is Linux and - # don't adjust the output path - # output_path = os.path.normpath(output_path) - return output_path - - -def getOutputPath(analysis): - if analysis and 'OutputPath' in analysis.PropertiesList: - output_path = analysis.OutputPath - else: - output_path = "" - if not output_path: - output_path = getDefaultOutputPath() - output_path = os.path.normpath(output_path) - return output_path - -def getRemoteOutputPath(analysis): - if analysis and 'RemoteOutputPath' in analysis.PropertiesList: - output_path = analysis.RemoteOutputPath - else: - output_path = "" - if not output_path: - output_path = getDefaultRemoteOutputPath() - # Assume the remote computer is Linux and don't - # adjust the path - # output_path = os.path.normpath(output_path) - return output_path - - -# Get functions -if FreeCAD.GuiUp: - def getResultObject(): - sel = FreeCADGui.Selection.getSelection() - if len(sel) == 1: - if sel[0].isDerivedFrom("Fem::FemResultObject"): - return sel[0] - for i in getActiveAnalysis().Group: - if i.isDerivedFrom("Fem::FemResultObject"): - return i - return None - - -def getParentAnalysisObject(obj): - """ - Return CfdAnalysis object to which this obj belongs in the tree - """ - from CfdOF import CfdAnalysis - parent = obj.getParentGroup() - if parent is None: - return None - elif hasattr(parent, 'Proxy') and isinstance(parent.Proxy, CfdAnalysis.CfdAnalysis): - return parent - else: - return getParentAnalysisObject(parent) - - -def getPhysicsModel(analysis_object): - is_present = False - for i in analysis_object.Group: - if "PhysicsModel" in i.Name: - physics_model = i - is_present = True - if not is_present: - physics_model = None - return physics_model - - -def getDynamicMeshAdaptation(analysis_object): - is_present = False - for i in getMesh(analysis_object).Group: - if "DynamicMeshInterfaceRefinement" in i.Name: - dynamic_mesh_adaption_model = i - is_present = True - if not is_present: - dynamic_mesh_adaption_model = None - return dynamic_mesh_adaption_model - - -def getMeshObject(analysis_object): - is_present = False - mesh_obj = [] - if analysis_object: - members = analysis_object.Group - else: - members = FreeCAD.activeDocument().Objects - from CfdOF.Mesh.CfdMesh import CfdMesh - for i in members: - if hasattr(i, "Proxy") and isinstance(i.Proxy, CfdMesh): - if is_present: - FreeCAD.Console.PrintError("Analysis contains more than one mesh object.") - else: - mesh_obj.append(i) - is_present = True - if not is_present: - mesh_obj = [None] - return mesh_obj[0] - - -def getPorousZoneObjects(analysis_object): - return [i for i in analysis_object.Group if i.Name.startswith('PorousZone')] - - -def getInitialisationZoneObjects(analysis_object): - return [i for i in analysis_object.Group if i.Name.startswith('InitialisationZone')] - - -def getZoneObjects(analysis_object): - return [i for i in analysis_object.Group if 'Zone' in i.Name] - - -def getInitialConditions(analysis_object): - from CfdOF.Solve.CfdInitialiseFlowField import CfdInitialVariables - for i in analysis_object.Group: - if isinstance(i.Proxy, CfdInitialVariables): - return i - return None - - -def getMaterials(analysis_object): - return [i for i in analysis_object.Group if i.isDerivedFrom('App::MaterialObjectPython')] - - -def getSolver(analysis_object): - from CfdOF.Solve.CfdSolverFoam import CfdSolverFoam - for i in analysis_object.Group: - if isinstance(i.Proxy, CfdSolverFoam): - return i - - -def getSolverSettings(solver): - """ - Convert properties into python dict, while key must begin with lower letter. - """ - dict = {} - f = lambda s: s[0].lower() + s[1:] - for prop in solver.PropertiesList: - dict[f(prop)] = solver.getPropertyByName(prop) - return dict - - -def getCfdBoundaryGroup(analysis_object): - group = [] - from CfdOF.Solve.CfdFluidBoundary import CfdFluidBoundary - for i in analysis_object.Group: - if isinstance(i.Proxy, CfdFluidBoundary): - group.append(i) - return group - - -def isPlanar(shape): - """ - Return whether the shape is a planar face - """ - n = shape.normalAt(0.5, 0.5) - if len(shape.Vertexes) <= 3: - return True - for v in shape.Vertexes[1:]: - t = v.Point - shape.Vertexes[0].Point - c = t.dot(n) - if c / t.Length > 1e-8: - return False - return True - - -def getMesh(analysis_object): - from CfdOF.Mesh.CfdMesh import CfdMesh - for i in analysis_object.Group: - if hasattr(i, "Proxy") and isinstance(i.Proxy, CfdMesh): - return i - return None - - -def getResult(analysis_object): - for i in analysis_object.Group: - if i.isDerivedFrom("Fem::FemResultObject"): - return i - return None - - -def getModulePath(): - """ - Returns the current Cfd module path. - Determines where this file is running from, so works regardless of whether - the module is installed in the app's module directory or the user's app data folder. - (The second overrides the first.) - """ - return os.path.normpath(os.path.join(os.path.dirname(__file__), os.path.pardir)) - - -# Function objects -def getReportingFunctionsGroup(analysis_object): - group = [] - from CfdOF.PostProcess.CfdReportingFunction import CfdReportingFunction - for i in analysis_object.Group: - if isinstance(i.Proxy, CfdReportingFunction): - group.append(i) - return group - - -def getScalarTransportFunctionsGroup(analysis_object): - group = [] - from CfdOF.Solve.CfdScalarTransportFunction import CfdScalarTransportFunction - for i in analysis_object.Group: - if isinstance(i.Proxy, CfdScalarTransportFunction): - group.append(i) - return group - - -# Mesh -def getMeshRefinementObjs(mesh_obj): - from CfdOF.Mesh.CfdMeshRefinement import CfdMeshRefinement - ref_objs = [] - for obj in mesh_obj.Group: - if hasattr(obj, "Proxy") and isinstance(obj.Proxy, CfdMeshRefinement): - ref_objs = ref_objs + [obj] - return ref_objs - - -# Set functions -def setCompSolid(vobj): - """ - To enable correct mesh refinement, boolean fragments are set to compSolid mode - """ - doc_name = str(vobj.Object.Document.Name) - doc = FreeCAD.getDocument(doc_name) - for obj in doc.Objects: - if hasattr(obj, 'Proxy') and isinstance(obj.Proxy, BOPTools.SplitFeatures.FeatureBooleanFragments): - FreeCAD.getDocument(doc_name).getObject(obj.Name).Mode = 'CompSolid' - - -def normalise(v): - import numpy - mag = numpy.sqrt(sum(vi**2 for vi in v)) - import sys - if mag < sys.float_info.min: - mag += sys.float_info.min - return [vi/mag for vi in v] - - -def cfdMessage(msg): - """ - Print a message to console and refresh GUI - """ - FreeCAD.Console.PrintMessage(msg) - if FreeCAD.GuiUp: - FreeCAD.Gui.updateGui() - FreeCAD.Gui.updateGui() - - -def cfdWarning(msg): - """ - Print a message to console and refresh GUI - """ - FreeCAD.Console.PrintWarning(msg) - if FreeCAD.GuiUp: - FreeCAD.Gui.updateGui() - FreeCAD.Gui.updateGui() - - -def cfdError(msg): - """ - Print a message to console and refresh GUI - """ - FreeCAD.Console.PrintError(msg) - if FreeCAD.GuiUp: - FreeCAD.Gui.updateGui() - FreeCAD.Gui.updateGui() - - -def cfdErrorBox(msg): - """ - Show message for an expected error - """ - QtGui.QApplication.restoreOverrideCursor() - if FreeCAD.GuiUp: - QtGui.QMessageBox.critical(None, "CfdOF Workbench", msg) - else: - FreeCAD.Console.PrintError(msg + "\n") - - -def formatTimer(seconds): - """ - Put the elapsed time printout into a nice format - """ - return str(timedelta(seconds=seconds)).split('.', 2)[0].lstrip('0').lstrip(':') - - -def getColour(type): - """ - type: 'Error', 'Warning', 'Logging', 'Text' - """ - col_int = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/OutputWindow").GetUnsigned('color'+type) - return '#{:08X}'.format(col_int)[:-2] - - -def setQuantity(inputField, quantity): - """ - Set the quantity (quantity object or unlocalised string) into the inputField correctly - """ - # Must set in the correctly localised value as the user would enter it. - # A bit painful because the python locale settings seem to be based on language, - # not input settings as the FreeCAD settings are. So can't use that; hence - # this rather roundabout way involving the UserString of Quantity - q = Units.Quantity(quantity) - # Avoid any truncation - if isinstance(q.Format, tuple): # Backward compat - q.Format = (12, 'e') - else: - q.Format = {'Precision': 12, 'NumberFormat': 'e', 'Denominator': q.Format['Denominator']} - inputField.setProperty("quantityString", q.UserString) - - -def getQuantity(inputField): - """ - Get the quantity as an unlocalised string from an inputField - """ - q = inputField.property("quantity") - return str(q) - - -def indexOrDefault(list, findItem, defaultIndex): - """ - Look for findItem in list, and return defaultIndex if not found - """ - try: - return list.index(findItem) - except ValueError: - return defaultIndex - - -def storeIfChanged(obj, prop, val): - cur_val = getattr(obj, prop) - if isinstance(cur_val, Units.Quantity): - if str(cur_val) != str(val): - FreeCADGui.doCommand("App.ActiveDocument.{}.{} = '{}'".format(obj.Name, prop, val)) - elif cur_val != val: - if isinstance(cur_val, str): - FreeCADGui.doCommand("App.ActiveDocument.{}.{} = '{}'".format(obj.Name, prop, val)) - elif isinstance(cur_val, FreeCAD.Vector): - FreeCADGui.doCommand("App.ActiveDocument.{}.{} = App.{}".format(obj.Name, prop, val)) - else: - FreeCADGui.doCommand("App.ActiveDocument.{}.{} = {}".format(obj.Name, prop, val)) - - -def copyFilesRec(src, dst, symlinks=False, ignore=None): - """ - Recursively copy files from src dir to dst dir - """ - if not os.path.exists(dst): - os.makedirs(dst) - for item in os.listdir(src): - s = os.path.join(src, item) - d = os.path.join(dst, item) - if not os.path.isdir(s): - shutil.copy2(s, d) - - -def getPatchType(bcType, bcSubType): - """ - Get the boundary type based on selected BC condition - """ - if bcType == 'wall': - return 'wall' - elif bcType == 'empty': - return 'empty' - elif bcType == 'constraint': - if bcSubType == 'symmetry': - return 'symmetry' - elif bcSubType == 'cyclic': - return 'cyclic' - elif bcSubType == 'wedge': - return 'wedge' - elif bcSubType == 'empty': - return 'empty' - else: - return 'patch' - else: - return 'patch' - - -def movePolyMesh(case): - """ - Move polyMesh to polyMesh.org to ensure availability if cleanCase is ran from the terminal. - """ - meshOrg_dir = case + os.path.sep + "constant/polyMesh.org" - mesh_dir = case + os.path.sep + "constant/polyMesh" - if os.path.isdir(meshOrg_dir): - shutil.rmtree(meshOrg_dir) - shutil.copytree(mesh_dir, meshOrg_dir) - shutil.rmtree(mesh_dir) - - -def getPreferencesLocation(): - # Set parameter location - return "User parameter:BaseApp/Preferences/Mod/CfdOF" - -def setFoamDir(installation_path): - prefs = getPreferencesLocation() - # Set OpenFOAM install path in parameters - FreeCAD.ParamGet(prefs).SetString("InstallationPath", installation_path) - -#not used anymore -def setRemoteFoamDir(remote_installation_path): - print("Error: setRemoteFoamDir is depreciated.") - prefs = getPreferencesLocation() - # Set OpenFOAM remote install path in parameters - FreeCAD.ParamGet(prefs).SetString("RemoteInstallationPath", remote_installation_path) - -# not used anymore -def getRemoteFoamDir(): - print("Error: getRemoteFoamDir is depreciated.") - prefs = getPreferencesLocation() - # Set OpenFOAM remote install path in parameters - return FreeCAD.ParamGet(prefs).GetString("RemoteInstallationPath", "") - -def startDocker(): - global docker_container - if docker_container==None: - docker_container = DockerContainer() - if docker_container.container_id==None: - if "podman" in docker_container.docker_cmd: - # Start podman machine if not already started - exit_code = checkPodmanMachineRunning() - if exit_code==2: - startPodmanMachine() - if checkPodmanMachineRunning(): - print("Aborting docker container initialization") - return 1 - elif exit_code==1: - print("Aborting docker container initialization") - return 1 - docker_container.start_container() - if docker_container.container_id != None: - print("Docker image {} started. ID = {}".format(docker_container.image_name, docker_container.container_id)) - return 0 - else: - print("Docker start appears to have failed") - return 1 - -def checkPodmanMachineRunning(): - print("Checking podman machine running") - cmd = "podman machine list" - proc = QtCore.QProcess() - proc.start(cmd) - proc.waitForFinished() - line = "" - while proc.canReadLine(): - line = str(proc.readLine().data(), encoding="utf-8") - if len(line)>0: - print(line) - if "Currently running" in line: - print("Podman machine running") - return 0 - elif line[-9:]=="DISK SIZE": - print("Podman machine not initialized - please refer to readme") - return 1 - else: - print("Podman machine not running") - return 2 - -def startPodmanMachine(): - print("Attempting podman machine start") - cmd = "podman machine start" - proc = QtCore.QProcess() - proc.start(cmd) - proc.waitForFinished() - line = "" - while proc.canReadLine(): - line = str(proc.readLine().data(), encoding="utf-8") - if len(line)>0: - print(line) - cmd = "podman machine set --rootful" - proc = QtCore.QProcess() - proc.start(cmd) - proc.waitForFinished() - line = "" - while proc.canReadLine(): - line = str(proc.readLine().data(), encoding="utf-8") - if len(line)>0: - print(line) - -def getFoamDir(): - global docker_container - if docker_container==None: - docker_container = DockerContainer() - if docker_container.usedocker: - return "" - - prefs = getPreferencesLocation() - # Get OpenFOAM install path from parameters - installation_path = FreeCAD.ParamGet(prefs).GetString("InstallationPath", "") - # Ensure parameters exist for future editing - setFoamDir(installation_path) - - # If not specified, try to detect from shell environment settings and defaults - if not installation_path: - installation_path = detectFoamDir() - - if installation_path: - installation_path = os.path.normpath(installation_path) - - return installation_path - - -def getFoamRuntime(): - global docker_container - if docker_container==None: - docker_container = DockerContainer() - if docker_container.usedocker: - return 'PosixDocker' - - installation_path = getFoamDir() - if installation_path is None: - raise IOError("OpenFOAM installation path not set and not detected") - - runtime = None - if platform.system() == 'Windows': - if os.path.exists(os.path.join(installation_path, "msys64", "home", "ofuser", ".blueCFDCore")): - runtime = 'BlueCFD' - elif os.path.exists(os.path.join(installation_path, "..", "msys64", "home", "ofuser", ".blueCFDCore")): - runtime = 'BlueCFD2' - elif os.path.exists(os.path.join(installation_path, "msys64", "home", "ofuser")): - runtime = 'MinGW' - elif os.path.exists(os.path.join(installation_path, "Windows", "Scripts")): - runtime = 'WindowsDocker' - elif os.path.exists(os.path.join(getFoamDir(), "etc", "bashrc")): - runtime = 'BashWSL' - else: - if not len(getFoamDir()): - runtime = 'PosixPreloaded' - if os.path.exists(os.path.join(getFoamDir(), "etc", "bashrc")): - runtime = 'Posix' - - if not runtime: - raise IOError("The directory {} is not a recognised OpenFOAM installation".format(installation_path)) - - return runtime - - -def findInDefaultPaths(paths): - for d in paths.get(platform.system(), []): - d = glob.glob(os.path.expandvars(os.path.expanduser(d))) - if len(d): - d = sorted(d)[-1] - if os.path.exists(d): - return d - return None - - -def detectFoamDir(): - """ - Try to guess Foam install dir from WM_PROJECT_DIR or, failing that, various defaults - """ - foam_dir = None - if platform.system() == "Linux": - # Detect pre-loaded environment - cmdline = ['bash', '-l', '-c', 'echo $WM_PROJECT_DIR'] - foam_dir = subprocess.check_output(cmdline, stderr=subprocess.PIPE, universal_newlines=True) - if len(foam_dir) > 1: # If env var is not defined, `\n` returned - foam_dir = foam_dir.strip() # Python2: Strip EOL char - else: - foam_dir = None - if foam_dir and not os.path.exists(os.path.join(foam_dir, "etc", "bashrc")): - foam_dir = None - if not foam_dir: - foam_dir = None - - if foam_dir is None: - foam_dir = findInDefaultPaths(FOAM_DIR_DEFAULTS) - return foam_dir - - -def setParaviewPath(paraview_path): - prefs = getPreferencesLocation() - # Set Paraview install path in parameters - FreeCAD.ParamGet(prefs).SetString("ParaviewPath", paraview_path) - -def getParaviewPath(): - prefs = getPreferencesLocation() - # Get path from parameters - paraview_path = FreeCAD.ParamGet(prefs).GetString("ParaviewPath", "") - # Ensure parameters exist for future editing - setParaviewPath(paraview_path) - return paraview_path - - -def setGmshPath(gmsh_path): - prefs = getPreferencesLocation() - # Set Paraview install path in parameters - FreeCAD.ParamGet(prefs).SetString("GmshPath", gmsh_path) - -# not used anymore -def setRemoteGmshPath(gmsh_path): - print("Error: setRemoteGmshPath is depreciated.") - prefs = getPreferencesLocation() - # Set Paraview install path in parameters - FreeCAD.ParamGet(prefs).SetString("RemoteGmshPath", gmsh_path) - -def getGmshPath(): - prefs = getPreferencesLocation() - # Get path from parameters - gmsh_path = FreeCAD.ParamGet(prefs).GetString("GmshPath", "") - # Ensure parameters exist for future editing - setGmshPath(gmsh_path) - return gmsh_path - -# not used anymore -def getRemoteGmshPath(): - print("Error: getRemoteGmshPath is depreciated.") - prefs = getPreferencesLocation() - # Get path from parameters - gmsh_path = FreeCAD.ParamGet(prefs).GetString("RemoteGmshPath", "") - # Ensure parameters exist for future editing - setGmshPath(gmsh_path) - return gmsh_path - - -def translatePath(p): - """ - Transform path to the perspective of the Linux subsystem in which OpenFOAM is run (e.g. mingw) - """ - if platform.system() == 'Windows': - return fromWindowsPath(p) - else: - return p - - -def reverseTranslatePath(p): - """ - Transform path from the perspective of the OpenFOAM subsystem to the host system - """ - if platform.system() == 'Windows': - return toWindowsPath(p) - else: - return p - - -def fromWindowsPath(p): - drive, tail = os.path.splitdrive(p) - pp = tail.replace('\\', '/') - if getFoamRuntime() == "MinGW" or getFoamRuntime() == "BlueCFD" or getFoamRuntime() == "BlueCFD2": - # Under mingw: c:\path -> /c/path - if os.path.isabs(p): - return "/" + (drive[:-1]).lower() + pp - else: - return pp - elif getFoamRuntime() == "WindowsDocker": - # Under docker: / -> /home/ofuser/workingDir/ - if os.path.isabs(p): - homepath = os.path.expanduser('~') - try: - if os.path.commonpath((os.path.normpath(p), homepath)) == homepath: - return '/home/ofuser/workingDir/' + os.path.relpath(p, homepath).replace('\\', '/') - else: - raise ValueError("The path {} is not inside the users's home directory.".format(p)) - except ValueError: - cfdError( - "The path {} cannot be used in the Docker environment. " - "Only paths inside the user's home directory are accessible.".format(p)) - raise - else: - return pp - elif getFoamRuntime() == "BashWSL": - # bash on windows: C:\Path -> /mnt/c/Path - if os.path.isabs(p): - return "/mnt/" + (drive[:-1]).lower() + pp - else: - return pp - else: # Nothing needed for posix - return p - - -def toWindowsPath(p): - pp = p.split('/') - if getFoamRuntime() == "MinGW": - # Under mingw: /c/path -> c:\path; /home/ofuser -> /msys64/home/ofuser - if p.startswith('/home/ofuser'): - return getFoamDir() + '\\msys64\\home\\ofuser\\' + '\\'.join(pp[3:]) - elif p.startswith('/'): - return pp[1].upper() + ':\\' + '\\'.join(pp[2:]) - else: - return p.replace('/', '\\') - elif getFoamRuntime() == "WindowsDocker": - # Under docker: /home/ofuser/workingDir/ -> / - homepath = os.path.expanduser('~') - if p.startswith('/home/ofuser/workingDir/'): - return os.path.join(homepath, "\\".join(pp[4:])) - else: - return p.replace('/', '\\') - elif getFoamRuntime() == "BashWSL": - # bash on windows: /mnt/c/Path -> C:\Path - if p.startswith('/mnt/'): - return pp[2].toupper() + ':\\' + '\\'.join(pp[3:]) - else: - return p.replace('/', '\\') - elif getFoamRuntime().startswith("BlueCFD"): - # Under blueCFD (mingw): /c/path -> c:\path; /home/ofuser/blueCFD -> - if p.startswith('/home/ofuser/blueCFD'): - if getFoamRuntime() == "BlueCFD2": - foam_dir = getFoamDir() + '\\' + '..' - else: - foam_dir = getFoamDir() - return foam_dir + '\\' + '\\'.join(pp[4:]) - elif p.startswith('/'): - return pp[1].upper() + ':\\' + '\\'.join(pp[2:]) - else: - return p.replace('/', '\\') - else: # Nothing needed for posix - return p - - -def getShortWindowsPath(long_name): - """ - Gets the short path name of a given long path. http://stackoverflow.com/a/23598461/200291 - """ - import ctypes - from ctypes import wintypes - _GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW - _GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD] - _GetShortPathNameW.restype = wintypes.DWORD - - output_buf_size = 0 - while True: - output_buf = ctypes.create_unicode_buffer(output_buf_size) - needed = _GetShortPathNameW(os.path.normpath(long_name), output_buf, output_buf_size) - if output_buf_size >= needed: - return output_buf.value - else: - output_buf_size = needed - - -def getRunEnvironment(): - """ - Return native environment settings necessary for running on relevant platform - """ - if getFoamRuntime() == "MinGW": - return {"MSYSTEM": "MSYS", - "USERNAME": "ofuser", - "USER": "ofuser", - "HOME": "/home/ofuser"} - elif getFoamRuntime().startswith("BlueCFD"): - return {"MSYSTEM": "MINGW64", - "USERNAME": "ofuser", - "USER": "ofuser", - "HOME": "/home/ofuser"} - else: - return {} - - -def makeRunCommand(cmd, dir, source_env=True): - """ - Generate native command to run the specified Linux command in the relevant environment, - including changing to the specified working directory if applicable - """ - - if getFoamRuntime() == "PosixDocker" and ' pull ' in cmd: - # Case where running from Install Docker thread - return cmd.split() - - installation_path = getFoamDir() - if installation_path is None: - raise IOError("OpenFOAM installation directory not found") - - FreeCAD.Console.PrintMessage('Executing: {} in {}\n'.format(cmd, dir)) - - source = "" - if source_env and len(installation_path): - env_setup_script = "{}/etc/bashrc".format(installation_path) - source = 'source "{}" && '.format(env_setup_script) - - if getFoamRuntime() == "PosixDocker": - # Set source for docker container - source = 'source /etc/bashrc && ' - - cd = "" - if dir: - cd = 'cd "{}" && '.format(translatePath(dir)) - - if getFoamRuntime() == "PosixDocker": - prefs = getPreferencesLocation() - if dir: - cd = cd.replace(FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", ""),'/tmp').replace('\\','/') - - if getFoamRuntime() == "MinGW": - # .bashrc will exit unless shell is interactive, so we have to manually load the foam bashrc - foamVersion = os.path.split(installation_path)[-1].lstrip('v') - cmdline = ['{}\\msys64\\usr\\bin\\bash'.format(installation_path), '--login', '-O', 'expand_aliases', '-c', - 'echo Sourcing OpenFOAM environment...; ' - 'source $HOME/OpenFOAM/OpenFOAM-v{}/etc/bashrc; '.format(foamVersion) + - 'export PATH=$FOAM_LIBBIN/msmpi:$FOAM_LIBBIN:$WM_THIRD_PARTY_DIR/platforms/linux64MingwDPInt32/lib:$PATH; ' - + cd + cmd] - return cmdline - - if getFoamRuntime() == "PosixDocker": - global docker_container - if docker_container.output_path_used!=FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", ""): - print("Output path changed - restarting container") - docker_container.stop_container() - docker_container.start_container() - if platform.system() == 'Windows' and FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "")[:5]=='\\\\wsl' and cmd[:5] == './All': - cmd = 'chmod 744 {0} && {0}'.format(cmd) # If using windows wsl$ output directory, need to make the command executable - cmdline = [docker_container.docker_cmd, 'exec', docker_container.container_id, 'bash', '-c', source + cd + cmd] - return cmdline - - if getFoamRuntime() == "WindowsDocker": - foamVersion = os.path.split(installation_path)[-1].lstrip('v') - cmdline = ['powershell.exe', - 'docker-machine.exe start default; ' - 'docker-machine.exe env --shell powershell default | Invoke-Expression; ' - 'docker start of_{}; '.format(foamVersion) + - 'docker exec --privileged of_{} '.format(foamVersion) + - 'bash -c "su -c \'' + # $ -> `$: escaping for powershell - (cd + cmd).replace('$', '`$').replace('"', '\\`"') + # Escape quotes for powershell and also cmdline to bash - '\' -l ofuser"'] - return cmdline - elif getFoamRuntime() == "BashWSL": - cmdline = ['bash', '-c', source + cd + cmd] - return cmdline - elif getFoamRuntime().startswith("BlueCFD"): - # Set-up necessary for running a command - only needs doing once, but to be safe... - if getFoamRuntime() == "BlueCFD2": - inst_path = "{}\\..".format(installation_path) - else: - inst_path = "{}".format(installation_path) - short_bluecfd_path = getShortWindowsPath(inst_path) - with open('{}\\msys64\\home\\ofuser\\.blueCFDOrigin'.format(inst_path), "w") as f: - f.write(short_bluecfd_path) - f.close() - srcdir = '{}\\msys64\\mingw64\\bin'.format(inst_path) - destdir1 = None - destdir2 = None - with os.scandir('{}'.format(inst_path)) as dirs: - for dir in dirs: - if dir.is_dir() and dir.name.startswith('OpenFOAM-'): - destdir1 = os.path.join(inst_path, dir.name, 'platforms\\mingw_w64GccDPInt32Opt\\bin') - if dir.is_dir() and dir.name.startswith('ofuser-of'): - destdir2 = os.path.join(inst_path, dir.name, 'platforms\\mingw_w64GccDPInt32Opt\\bin') - if not destdir1 or not destdir2: - cfdError("Unable to find directories 'OpenFOAM-*' and 'ofuser-of*' in path {}. " - "Possible error in BlueCFD installation.".format(inst_path)) - try: - file = 'libstdc++-6.dll' - if destdir1 and not os.path.isfile(os.path.join(destdir1, file)): - shutil.copy(os.path.join(srcdir, file), os.path.join(destdir1, file)) - if destdir2 and not os.path.isfile(os.path.join(destdir2, file)): - shutil.copy(os.path.join(srcdir, file), os.path.join(destdir2, file)) - file = 'libgomp-1.dll' - if not os.path.isfile(os.path.join(destdir1, file)): - shutil.copy(os.path.join(srcdir, file), os.path.join(destdir1, file)) - if not os.path.isfile(os.path.join(destdir2, file)): - shutil.copy(os.path.join(srcdir, file), os.path.join(destdir2, file)) - except IOError as err: - cfdError("Unable to copy file {} from directory {} to {} and {}: {}\n" - "Try running FreeCAD again with administrator privileges, or copy the file manually." - .format(file, srcdir, destdir1, destdir2, str(err))) - - # Note: Prefixing bash call with the *short* path can prevent errors due to spaces in paths - # when running linux tools - specifically when building - cmdline = ['{}\\msys64\\usr\\bin\\bash'.format(short_bluecfd_path), '--login', '-O', 'expand_aliases', '-c', - cd + cmd] - return cmdline - else: - cmdline = ['bash', '-c', source + cd + cmd] - return cmdline - - -def runFoamCommand(cmdline, case=None): - """ - Run a command in the OpenFOAM environment and wait until finished. Return output as (stdout, stderr, combined) - Also print output as we go. - cmdline - The command line to run as a string - e.g. transformPoints -scale "(0.001 0.001 0.001)" - case - Case directory or path - """ - proc = CfdSynchronousFoamProcess() - exit_code = proc.run(cmdline, case) - # Reproduce behaviour of failed subprocess run - if exit_code: - raise subprocess.CalledProcessError(exit_code, cmdline) - return proc.output, proc.outputErr, proc.outputAll - - - -def startFoamApplication(cmd, case, log_name='', finished_hook=None, stdout_hook=None, stderr_hook=None): - """ - Run command cmd in OpenFOAM environment, sending output to log file. - Returns a CfdConsoleProcess object after launching - cmd - List or string with the application being the first entry followed by the options. - e.g. ['transformPoints', '-scale', '"(0.001 0.001 0.001)"'] - case - Case path - log_name - File name to pipe output to, if not None. If zero-length string, will generate automatically - as log. where is the first element in cmd. - """ - if isinstance(cmd, list) or isinstance(cmd, tuple): - cmds = cmd - elif isinstance(cmd, str): - cmds = cmd.split(' ') # Insensitive to incorrect split like space and quote - else: - raise Exception("Error: Application and options must be specified as a list or tuple.") - - if log_name == '': - app = cmds[0].rsplit('/', 1)[-1] - logFile = "log.{}".format(app) - else: - logFile = log_name - - cmdline = ' '.join(cmds) # Space to separate options - # Pipe to log file and terminal - if logFile: - cmdline += " 1> >(tee -a " + logFile + ") 2> >(tee -a " + logFile + " >&2)" - # Tee appends to the log file, so we must remove first. Can't do directly since - # paths may be specified using variables only available in foam runtime environment. - cmdline = "{{ rm -f {}; {}; }}".format(logFile, cmdline) - - proc = CfdConsoleProcess(finished_hook=finished_hook, stdout_hook=stdout_hook, stderr_hook=stderr_hook) - if logFile: - print("Running ", ' '.join(cmds), " -> ", logFile) - else: - print("Running ", ' '.join(cmds)) - - proc.start(makeRunCommand(cmdline, case), env_vars=getRunEnvironment()) - if not proc.waitForStarted(): - raise Exception("Unable to start command " + ' '.join(cmds)) - return proc - - -def runFoamApplication(cmd, case, log_name=''): - """ - Same as startFoamApplication, but waits until complete. Returns exit code. - """ - proc = startFoamApplication(cmd, case, log_name) - proc.waitForFinished() - return proc.exitCode() - - -def convertMesh(case, mesh_file, scale): - """ - Convert gmsh created UNV mesh to FOAM. A scaling of 1e-3 is prescribed as the CAD is always in mm while FOAM - uses SI units (m). - """ - - if mesh_file.find(".unv") > 0: - mesh_file = translatePath(mesh_file) - cmdline = ['ideasUnvToFoam', '"{}"'.format(mesh_file)] - runFoamApplication(cmdline, case) - # changeBoundaryType(case, 'defaultFaces', 'wall') # rename default boundary type to wall - # Set in the correct patch types - cmdline = ['changeDictionary'] - runFoamApplication(cmdline, case) - else: - raise Exception("Error: Only supporting unv mesh files.") - - if scale and isinstance(scale, numbers.Number): - cmdline = ['transformPoints', '-scale', '"({} {} {})"'.format(scale, scale, scale)] - runFoamApplication(cmdline, case) - else: - print("Error: mesh scaling ratio is must be a float or integer\n") - - -def checkCfdDependencies(): - FC_MAJOR_VER_REQUIRED = 0 - FC_MINOR_VER_REQUIRED = 18 - FC_PATCH_VER_REQUIRED = 4 - FC_COMMIT_REQUIRED = 16146 - - CF_MAJOR_VER_REQUIRED = 1 - CF_MINOR_VER_REQUIRED = 16 - - HISA_MAJOR_VER_REQUIRED = 1 - HISA_MINOR_VER_REQUIRED = 6 - HISA_PATCH_VER_REQUIRED = 4 - - message = "" - FreeCAD.Console.PrintMessage("Checking CFD workbench dependencies...\n") - - # Check FreeCAD version - print("Checking FreeCAD version") - ver = FreeCAD.Version() - major_ver = int(ver[0]) - minor_vers = ver[1].split('.') - minor_ver = int(minor_vers[0]) - if minor_vers[1:] and minor_vers[1]: - patch_ver = int(minor_vers[1]) - else: - patch_ver = 0 - gitver = ver[2].split() - if gitver: - gitver = gitver[0] - if gitver and gitver != 'Unknown': - gitver = int(gitver) - else: - # If we don't have the git version, assume it's OK. - gitver = FC_COMMIT_REQUIRED - - if (major_ver < FC_MAJOR_VER_REQUIRED or - (major_ver == FC_MAJOR_VER_REQUIRED and - (minor_ver < FC_MINOR_VER_REQUIRED or - (minor_ver == FC_MINOR_VER_REQUIRED and - (patch_ver < FC_PATCH_VER_REQUIRED or - (patch_ver == FC_PATCH_VER_REQUIRED and - gitver < FC_COMMIT_REQUIRED)))))): - fc_msg = "FreeCAD version ({}.{}.{}) ({}) must be at least {}.{}.{} ({})".format( - int(ver[0]), minor_ver, patch_ver, gitver, - FC_MAJOR_VER_REQUIRED, FC_MINOR_VER_REQUIRED, FC_PATCH_VER_REQUIRED, FC_COMMIT_REQUIRED) - print(fc_msg) - message += fc_msg + '\n' - - # check openfoam - print("Checking for OpenFOAM:") - try: - foam_dir = getFoamDir() - sys_msg = "System: {}\nRuntime: {}\nOpenFOAM directory: {}".format( - platform.system(), getFoamRuntime(), foam_dir if len(foam_dir) else "(system installation)") - print(sys_msg) - message += sys_msg + '\n' - except IOError as e: - ofmsg = "Could not find OpenFOAM installation: " + str(e) - print(ofmsg) - message += ofmsg + '\n' - else: - if foam_dir is None: - ofmsg = "OpenFOAM installation path not set and OpenFOAM environment neither pre-loaded before " + \ - "running FreeCAD nor detected in standard locations" - print(ofmsg) - message += ofmsg + '\n' - else: - if getFoamRuntime() == "PosixDocker": - startDocker() - try: - if getFoamRuntime() == "MinGW": - foam_ver = runFoamCommand("echo $FOAM_API")[0] - else: - foam_ver = runFoamCommand("echo $WM_PROJECT_VERSION")[0] - except Exception as e: - runmsg = "OpenFOAM installation found, but unable to run command: " + str(e) - message += runmsg + '\n' - print(runmsg) - raise - else: - foam_ver = foam_ver.rstrip() - if foam_ver: - foam_ver = foam_ver.split()[-1] - if foam_ver and foam_ver != 'dev' and foam_ver != 'plus': - try: - # Isolate major version number - foam_ver = foam_ver.lstrip('v') - foam_ver = int(foam_ver.split('.')[0]) - if getFoamRuntime() == "MinGW": - if foam_ver < 2012 or foam_ver > 2206: - vermsg = "OpenFOAM version " + str(foam_ver) + \ - " is not currently supported with MinGW installation" - message += vermsg + "\n" - print(vermsg) - if foam_ver >= 1000: # Plus version - if foam_ver < 1706: - vermsg = "OpenFOAM version " + str(foam_ver) + " is outdated:\n" + \ - "Minimum version 1706 or 5 required" - message += vermsg + "\n" - print(vermsg) - if foam_ver > 2206: - vermsg = "OpenFOAM version " + str(foam_ver) + " is not yet supported:\n" + \ - "Last tested version is 2206" - message += vermsg + "\n" - print(vermsg) - else: # Foundation version - if foam_ver < 5: - vermsg = "OpenFOAM version " + str(foam_ver) + " is outdated:\n" + \ - "Minimum version 5 or 1706 required" - message += vermsg + "\n" - print(vermsg) - if foam_ver > 9: - vermsg = "OpenFOAM version " + str(foam_ver) + " is not yet supported:\n" + \ - "Last tested version is 9" - message += vermsg + "\n" - print(vermsg) - except ValueError: - vermsg = "Error parsing OpenFOAM version string " + foam_ver - message += vermsg + "\n" - print(vermsg) - # Check for wmake - if getFoamRuntime() != "MinGW" and getFoamRuntime() != "PosixDocker": - try: - runFoamCommand("wmake -help") - except subprocess.CalledProcessError: - wmakemsg = "OpenFOAM installation does not include 'wmake'. " + \ - "Installation of cfMesh and HiSA will not be possible." - message += wmakemsg + "\n" - print(wmakemsg) - - # Check for cfMesh - try: - cfmesh_ver = runFoamCommand("cartesianMesh -version")[0] - cfmesh_ver = cfmesh_ver.rstrip().split()[-1] - cfmesh_ver = cfmesh_ver.split('.') - if (not cfmesh_ver or len(cfmesh_ver) != 2 or - int(cfmesh_ver[0]) < CF_MAJOR_VER_REQUIRED or - (int(cfmesh_ver[0]) == CF_MAJOR_VER_REQUIRED and - int(cfmesh_ver[1]) < CF_MINOR_VER_REQUIRED)): - vermsg = "cfMesh-CfdOF version {}.{} required".format(CF_MAJOR_VER_REQUIRED, - CF_MINOR_VER_REQUIRED) - message += vermsg + "\n" - print(vermsg) - except subprocess.CalledProcessError: - cfmesh_msg = "cfMesh (CfdOF version) not found" - message += cfmesh_msg + '\n' - print(cfmesh_msg) - - # Check for HiSA - try: - hisa_ver = runFoamCommand("hisa -version")[0] - hisa_ver = hisa_ver.rstrip().split()[-1] - hisa_ver = hisa_ver.split('.') - if (not hisa_ver or len(hisa_ver) != 3 or - int(hisa_ver[0]) < HISA_MAJOR_VER_REQUIRED or - (int(hisa_ver[0]) == HISA_MAJOR_VER_REQUIRED and - (int(hisa_ver[1]) < HISA_MINOR_VER_REQUIRED or - (int(hisa_ver[1]) == HISA_MINOR_VER_REQUIRED and - int(hisa_ver[2]) < HISA_PATCH_VER_REQUIRED)))): - vermsg = "HiSA version {}.{}.{} required".format(HISA_MAJOR_VER_REQUIRED, - HISA_MINOR_VER_REQUIRED, - HISA_PATCH_VER_REQUIRED) - message += vermsg + "\n" - print(vermsg) - except subprocess.CalledProcessError: - hisa_msg = "HiSA not found" - message += hisa_msg + '\n' - print(hisa_msg) - - # Check for paraview - print("Checking for paraview:") - paraview_cmd = getParaviewExecutable() - failed = False - if not paraview_cmd: - paraview_cmd = 'paraview' - # If not found, try to run from the OpenFOAM environment, in case a bundled version is - # available from there - try: - runFoamCommand('which paraview') - except subprocess.CalledProcessError: - failed = True - if failed or not os.path.exists(paraview_cmd): - pv_msg = "Paraview executable '" + paraview_cmd + "' not found." - message += pv_msg + '\n' - print(pv_msg) - else: - pv_msg = "Paraview executable: {}".format(paraview_cmd) - message += pv_msg + '\n' - print(pv_msg) - - # Check for paraview python support - if not failed: - failed = False - paraview_cmd = getParaviewExecutable() - if not paraview_cmd: - pvpython_cmd = 'pvpython' - # If not found, try to run from the OpenFOAM environment, in case a bundled version is - # available from there - try: - runFoamCommand('which pvpython') - except subprocess.CalledProcessError: - failed = True - else: - if platform.system() == 'Windows': - pvpython_cmd = paraview_cmd.rstrip('paraview.exe')+'pvpython.exe' - else: - pvpython_cmd = paraview_cmd.rstrip('paraview')+'pvpython' - if failed or not os.path.exists(pvpython_cmd): - pv_msg = "Python support in paraview not found. Please install paraview python packages." - message += pv_msg + '\n' - print(pv_msg) - - print("Checking Plot module:") - - try: - import matplotlib - except ImportError: - matplot_msg = "Could not load matplotlib package (required by Plot module)" - message += matplot_msg + '\n' - print(matplot_msg) - - plot_ok = False - if major_ver > 0 or minor_ver >= 20: - try: - from FreeCAD.Plot import Plot # Built-in plot module - plot_ok = True - except ImportError: - plot_msg = "Could not load Plot module\nAttempting to use Plot workbench instead" - message += plot_msg + "\n" - print(plot_msg) - if not plot_ok: - try: - from CfdOF.compat import Plot # Plot workbench - except ImportError: - plot_msg = "Could not load legacy Plot module" - message += plot_msg + '\n' - print(plot_msg) - - print("Checking for gmsh:") - # check that gmsh version 2.13 or greater is installed - gmshversion = "" - gmsh_exe = getGmshExecutable() - if gmsh_exe is None: - gmsh_msg = "gmsh not found (optional)" - message += gmsh_msg + '\n' - print(gmsh_msg) - else: - gmsh_msg = "gmsh executable: " + gmsh_exe - message += gmsh_msg + '\n' - print(gmsh_msg) - try: - # Needs to be runnable from OpenFOAM environment - gmshversion = runFoamCommand("'" + gmsh_exe + "'" + " -version")[2] - except (OSError, subprocess.CalledProcessError): - gmsh_msg = "gmsh could not be run from OpenFOAM environment" - message += gmsh_msg + '\n' - print(gmsh_msg) - if len(gmshversion) > 1: - # Only the last line contains gmsh version number - gmshversion = gmshversion.rstrip().split() - gmshversion = gmshversion[-1] - versionlist = gmshversion.split(".") - if int(versionlist[0]) < 2 or (int(versionlist[0]) == 2 and int(versionlist[1]) < 13): - gmsh_ver_msg = "gmsh version is older than minimum required (2.13)" - message += gmsh_ver_msg + '\n' - print(gmsh_ver_msg) - - print("Completed CFD dependency check") - return message - -#****************************************************************************** -# Done: TODO: right now this routine returns the error code of the outermost process, ie the one called with cmd parameter. -# So if you call ssh, for example, and it runs successfully, it will return no error, no matter what happens to the command -# that was used in ssh. This should be changed. If one gets an error message via stderr, this routine should -# return an error code, regardless of how ssh itself ran. -# -# TODO: add a timer so that the process times out if it doesn't create any output or do anything. -# If ssh credentials aren't set up properly on the remote host, it will ask for a password and not echo that to stdout or stderr. -# Thus the process will be stuck. -# -# This routine is not needed. runFoamCommand does the same thing but is integrated into CfdTools already. -# The only routines that use runCommand are ping and ssh test in RemotePreferences.py. Those routines should be -# rewritten with runFoamCommand and put in CfdTools. -# -# When that happens, this routine can be deleted. - -def runCommand(cmd, args=[]): - - process = QProcess() - returnValue = 0 - - def handleStdout(): - #print("In Stdout handler") - data = process.readAllStandardOutput() - message = bytes(data).decode("utf8") - print(message) - - def handleStderr(): - nonlocal returnValue - #print("In Stderr handler") - data = process.readAllStandardError() - message = bytes(data).decode("utf8") - print("Error:" + message) - returnValue = -1 - - def handleFinished(self, exitCode): - nonlocal returnValue - print("Command done.") - if ((exitCode == QProcess.NormalExit) and (returnValue == 0)): - returnValue = 0 - else: - returnValue = -1 - - # connect the events to handlers - process.readyReadStandardOutput.connect(handleStdout) - process.readyReadStandardError.connect(handleStderr) - process.finished.connect(handleFinished) - - # debug - message = "Remotely executing: " + cmd - for arg in args: - message = message + " " + arg - print(message) - - # start the command process - process.start(cmd, args) - - # wait for the process to get started - process.waitForStarted() - - # process application events while waiting for it to finish - while(process.state() == QProcess.Running): - QApplication.processEvents() - return(returnValue) - - -def getParaviewExecutable(): - # If path of paraview executable specified, use that - paraview_cmd = getParaviewPath() - if not paraview_cmd: - # If using blueCFD, use paraview supplied - if getFoamRuntime() == 'BlueCFD': - paraview_cmd = '{}\\AddOns\\ParaView\\bin\\paraview.exe'.format(getFoamDir()) - elif getFoamRuntime() == 'BlueCFD2': - paraview_cmd = '{}\\..\\AddOns\\ParaView\\bin\\paraview.exe'.format(getFoamDir()) - else: - # Check the defaults - paraview_cmd = findInDefaultPaths(PARAVIEW_PATH_DEFAULTS) - if not paraview_cmd: - # Otherwise, see if the command 'paraview' is in the path. - paraview_cmd = shutil.which('paraview') - return paraview_cmd - - -def getGmshExecutable(): - # If path of gmsh executable specified, use that - gmsh_cmd = getGmshPath() - if not gmsh_cmd: - # On Windows, use gmsh supplied - if platform.system() == "Windows": - # Use forward slashes to avoid escaping problems - gmsh_cmd = '/'.join([FreeCAD.getHomePath().rstrip('/'), 'bin', 'gmsh.exe']) - if not gmsh_cmd: - # Otherwise, see if the command 'gmsh' is in the path. - gmsh_cmd = shutil.which("gmsh") - if getFoamRuntime() == "PosixDocker": - gmsh_cmd='gmsh' - return gmsh_cmd - -def getRemoteGmshExecutable(): - # If path of gmsh executable specified, use that - gmsh_cmd = getGmshPath() - if not gmsh_cmd: - # On Windows, use gmsh supplied - if platform.system() == "Windows": - # Use forward slashes to avoid escaping problems - gmsh_cmd = '/'.join([FreeCAD.getHomePath().rstrip('/'), 'bin', 'gmsh.exe']) - if not gmsh_cmd: - # Otherwise, see if the command 'gmsh' is in the path. - gmsh_cmd = shutil.which("gmsh") - if getFoamRuntime() == "PosixDocker": - gmsh_cmd='gmsh' - return gmsh_cmd - - - -def startParaview(case_path, script_name, console_message_fn): - proc = QtCore.QProcess() - paraview_cmd = getParaviewExecutable() - arg = '--script={}'.format(script_name) - - if not paraview_cmd: - # If not found, try to run from the OpenFOAM environment, in case a bundled version is available from there - paraview_cmd = "$(which paraview)" # 'which' required due to mingw weirdness(?) on Windows - try: - cmds = [paraview_cmd, arg] - cmd = ' '.join(cmds) - console_message_fn("Running " + cmd) - args = makeRunCommand(cmd, case_path) - paraview_cmd = args[0] - args = args[1:] if len(args) > 1 else [] - proc.setProgram(paraview_cmd) - proc.setArguments([arg]) - proc.setProcessEnvironment(getRunEnvironment()) - success = proc.startDetached() - if not success: - raise Exception("Unable to start command " + cmd) - console_message_fn("Paraview started") - except QtCore.QProcess.ProcessError: - console_message_fn("Error starting paraview") - else: - console_message_fn("Running " + paraview_cmd + " " + arg) - proc.setProgram(paraview_cmd) - proc.setArguments([arg]) - proc.setWorkingDirectory(case_path) - env = QtCore.QProcessEnvironment.systemEnvironment() - removeAppimageEnvironment(env) - proc.setProcessEnvironment(env) - success = proc.startDetached() - if success: - console_message_fn("Paraview started") - else: - console_message_fn("Error starting paraview") - return success - - -def startGmsh(working_dir, args, console_message_fn, stdout_fn=None, stderr_fn=None): - proc = CfdConsoleProcess(stdout_hook=stdout_fn, stderr_hook=stderr_fn) - gmsh_cmd = getGmshExecutable() - - if not gmsh_cmd: - console_message_fn("GMSH not found") - else: - console_message_fn("Running " + gmsh_cmd + " " + ' '.join(args)) - - proc.start([gmsh_cmd] + args, working_dir=working_dir) - if proc.waitForStarted(): - console_message_fn("GMSH started") - else: - console_message_fn("Error starting GMSH") - return proc - - -def floatEqual(a, b): - """ - Test whether a and b are equal within an absolute and relative tolerance - """ - reltol = 10*sys.float_info.epsilon - abstol = 1e-12 # Seems to be necessary on file read/write - return abs(a-b) < abstol or abs(a - b) <= reltol*max(abs(a), abs(b)) - - -def isSameGeometry(shape1, shape2): - """ - Copy of FemMeshTools.is_same_geometry, with fixes - """ - # Check Area, CenterOfMass because non-planar shapes might not have more than one vertex defined - same_Vertexes = 0 - # Bugfix: below was 1 - did not work for non-planar shapes - if len(shape1.Vertexes) == len(shape2.Vertexes) and len(shape1.Vertexes) > 0: - # compare CenterOfMass - # Bugfix: Precision seems to be lost on load/save - if hasattr(shape1, "CenterOfMass") and hasattr(shape2, "CenterOfMass"): - if not floatEqual(shape1.CenterOfMass[0], shape2.CenterOfMass[0]) or \ - not floatEqual(shape1.CenterOfMass[1], shape2.CenterOfMass[1]) or \ - not floatEqual(shape1.CenterOfMass[2], shape2.CenterOfMass[2]): - return False - if hasattr(shape1, "Area") and hasattr(shape2, "Area"): - if not floatEqual(shape1.Area, shape2.Area): - return False - # compare the Vertices - for vs1 in shape1.Vertexes: - for vs2 in shape2.Vertexes: - if floatEqual(vs1.X, vs2.X) and floatEqual(vs1.Y, vs2.Y) and floatEqual(vs1.Z, vs2.Z): - same_Vertexes += 1 - # Bugfix: was 'continue' - caused false-negative with repeated vertices - break - if same_Vertexes == len(shape1.Vertexes): - return True - else: - return False - - -def findElementInShape(a_shape, an_element): - """ - Copy of FemMeshTools.find_element_in_shape, but calling isSameGeometry - """ - # import Part - ele_st = an_element.ShapeType - if ele_st == 'Solid' or ele_st == 'CompSolid': - for index, solid in enumerate(a_shape.Solids): - # print(is_same_geometry(solid, anElement)) - if isSameGeometry(solid, an_element): - # print(index) - # Part.show(aShape.Solids[index]) - ele = ele_st + str(index + 1) - return ele - FreeCAD.Console.PrintError('Solid ' + str(an_element) + ' not found in: ' + str(a_shape) + '\n') - if ele_st == 'Solid' and a_shape.ShapeType == 'Solid': - print('We have been searching for a Solid in a Solid and we have not found it. In most cases this should be searching for a Solid inside a CompSolid. Check the ShapeType of your Part to mesh.') - # Part.show(anElement) - # Part.show(aShape) - elif ele_st == 'Face' or ele_st == 'Shell': - for index, face in enumerate(a_shape.Faces): - # print(is_same_geometry(face, anElement)) - if isSameGeometry(face, an_element): - # print(index) - # Part.show(aShape.Faces[index]) - ele = ele_st + str(index + 1) - return ele - elif ele_st == 'Edge' or ele_st == 'Wire': - for index, edge in enumerate(a_shape.Edges): - # print(is_same_geometry(edge, anElement)) - if isSameGeometry(edge, an_element): - # print(index) - # Part.show(aShape.Edges[index]) - ele = ele_st + str(index + 1) - return ele - elif ele_st == 'Vertex': - for index, vertex in enumerate(a_shape.Vertexes): - # print(is_same_geometry(vertex, anElement)) - if isSameGeometry(vertex, an_element): - # print(index) - # Part.show(aShape.Vertexes[index]) - ele = ele_st + str(index + 1) - return ele - elif ele_st == 'Compound': - FreeCAD.Console.PrintError('Compound is not supported.\n') - - -def matchFaces(faces1, faces2): - """ - This function does a geometric matching of face lists much faster than doing face-by-face search - :param faces1: List of tuples - first item is face object, second is any user data - :param faces2: List of tuples - first item is face object, second is any user data - :return: A list of (data1, data2) containing the user data for any/all matching faces - Note that faces1 and faces2 are sorted in place and can be re-used for faster subsequent searches - """ - - if sys.version_info >= (3,): # Python 3 - - def compKeyFn(key): - class K(object): - def __init__(self, val, *args): - self.val = key(val) - - def __eq__(self, other): - return floatEqual(self.val, other.val) - - def __ne__(self, other): - return not floatEqual(self.val, other.val) - - def __lt__(self, other): - return self.val < other.val and not floatEqual(self.val, other.val) - - def __gt__(self, other): - return self.val > other.val and not floatEqual(self.val, other.val) - - def __le__(self, other): - return self.val < other.val or floatEqual(self.val, other.val) - - def __ge__(self, other): - return self.val > other.val or floatEqual(self.val, other.val) - - return K - - # Sort face list by first vertex, x then y then z in case all in plane - faces1.sort(key=compKeyFn(lambda bf: bf[0].Vertexes[0].Point.z)) - faces1.sort(key=compKeyFn(lambda bf: bf[0].Vertexes[0].Point.y)) - faces1.sort(key=compKeyFn(lambda bf: bf[0].Vertexes[0].Point.x)) - - # Same on other face list - faces2.sort(key=compKeyFn(lambda mf: mf[0].Vertexes[0].Point.z)) - faces2.sort(key=compKeyFn(lambda mf: mf[0].Vertexes[0].Point.y)) - faces2.sort(key=compKeyFn(lambda mf: mf[0].Vertexes[0].Point.x)) - - else: # Python 2 - - def compFn(x, y): - if floatEqual(x, y): - return 0 - elif x < y: - return -1 - else: - return 1 - - # Sort face list by first vertex, x then y then z in case all in plane - faces1.sort(cmp=compFn, key=lambda bf: bf[0].Vertexes[0].Point.z) - faces1.sort(cmp=compFn, key=lambda bf: bf[0].Vertexes[0].Point.y) - faces1.sort(cmp=compFn, key=lambda bf: bf[0].Vertexes[0].Point.x) - - # Same on other face list - faces2.sort(cmp=compFn, key=lambda mf: mf[0].Vertexes[0].Point.z) - faces2.sort(cmp=compFn, key=lambda mf: mf[0].Vertexes[0].Point.y) - faces2.sort(cmp=compFn, key=lambda mf: mf[0].Vertexes[0].Point.x) - - # Find faces with matching first vertex - i = 0 - j = 0 - j_match_start = 0 - matching = False - candidate_mesh_faces = [] - while i < len(faces1) and j < len(faces2): - bf = faces1[i][0] - mf = faces2[j][0] - if floatEqual(bf.Vertexes[0].Point.x, mf.Vertexes[0].Point.x): - if floatEqual(bf.Vertexes[0].Point.y, mf.Vertexes[0].Point.y): - if floatEqual(bf.Vertexes[0].Point.z, mf.Vertexes[0].Point.z): - candidate_mesh_faces.append((i, j)) - cmp = 0 - else: - cmp = (-1 if bf.Vertexes[0].Point.z < mf.Vertexes[0].Point.z else 1) - else: - cmp = (-1 if bf.Vertexes[0].Point.y < mf.Vertexes[0].Point.y else 1) - else: - cmp = (-1 if bf.Vertexes[0].Point.x < mf.Vertexes[0].Point.x else 1) - if cmp == 0: - if not matching: - j_match_start = j - j += 1 - matching = True - if j == len(faces2): - i += 1 - j = j_match_start - matching = False - elif cmp < 0: - i += 1 - if matching: - j = j_match_start - matching = False - elif cmp > 0: - j += 1 - matching = False - - # Do comprehensive matching, and reallocate to original index - successful_candidates = [] - for k in range(len(candidate_mesh_faces)): - i, j = candidate_mesh_faces[k] - if isSameGeometry(faces1[i][0], faces2[j][0]): - successful_candidates.append((faces1[i][1], faces2[j][1])) - - return successful_candidates - - -def makeShapeFromReferences(refs, raise_error=True): - face_list = [] - for ref in refs: - shapes = resolveReference(ref, raise_error) - if len(shapes): - face_list += [s[0] for s in shapes] - if len(face_list) > 0: - shape = Part.makeCompound(face_list) - return shape - else: - return None - - -def resolveReference(r, raise_error=True): - obj = r[0] - if not r[1] or r[1] == ('',): - return [(obj.Shape, (r[0], None))] - f = [] - for rr in r[1]: - try: - if rr.startswith('Solid'): # getElement doesn't work with solids for some reason - f += [(obj.Shape.Solids[int(rr.lstrip('Solid')) - 1], (r[0], rr))] - else: - ff = obj.Shape.getElement(rr) - if ff is None: - if raise_error: - raise RuntimeError("Face '{}:{}' was not found - geometry may have changed".format(r[0].Name, rr)) - else: - f += [(ff, (r[0], rr))] - except Part.OCCError: - if raise_error: - raise RuntimeError("Face '{}:{}' was not found - geometry may have changed".format(r[0].Name, r[1])) - return f - - -def setActiveAnalysis(analysis): - from CfdOF.CfdAnalysis import CfdAnalysis - for obj in FreeCAD.ActiveDocument.Objects: - if hasattr(obj, 'Proxy') and isinstance(obj.Proxy, CfdAnalysis): - obj.IsActiveAnalysis = False - - analysis.IsActiveAnalysis = True - - -def getActiveAnalysis(): - from CfdOF.CfdAnalysis import CfdAnalysis - for obj in FreeCAD.ActiveDocument.Objects: - if hasattr(obj, 'Proxy') and isinstance(obj.Proxy, CfdAnalysis): - if obj.IsActiveAnalysis: - return obj - return None - - -def addObjectProperty(obj, prop, init_val, type, *args): - """ - Call addProperty on the object if it does not yet exist - """ - added = False - if prop not in obj.PropertiesList: - added = obj.addProperty(type, prop, *args) - if type == 'App::PropertyQuantity': - # Set the unit so that the quantity will be accepted - # Has to be repeated on load as unit gets lost - setattr(obj, prop, Units.Unit(init_val)) - if added: - setattr(obj, prop, init_val) - elif type == 'App::PropertyEnumeration': - # For enumeration, re-assign the list of allowed values anyway in case some were added - # Make sure the currently set value is unaffected by this - curr_item = getattr(obj, prop) - setattr(obj, prop, init_val) - setattr(obj, prop, curr_item) - return added - - -def relLenToRefinementLevel(rel_len): - return math.ceil(math.log(1.0/rel_len)/math.log(2)) - - -def importMaterials(): - materials = {} - material_name_path_list = [] - - # Store the defaults inside the module directory rather than the resource dir - # system_mat_dir = FreeCAD.getResourceDir() + "/Mod/Material/FluidMaterialProperties" - system_mat_dir = os.path.join(getModulePath(), "Data", "CfdFluidMaterialProperties") - material_name_path_list = material_name_path_list + addMatDir(system_mat_dir, materials) - return materials, material_name_path_list - - -def addMatDir(mat_dir, materials): - import importFCMat - mat_file_extension = ".FCMat" - ext_len = len(mat_file_extension) - dir_path_list = glob.glob(mat_dir + '/*' + mat_file_extension) - material_name_path_list = [] - for a_path in dir_path_list: - material_name = os.path.basename(a_path[:-ext_len]) - materials[a_path] = importFCMat.read(a_path) - material_name_path_list.append([material_name, a_path]) - material_name_path_list.sort() - - return material_name_path_list - - -def propsToDict(obj): - """ - Convert an object's properties to dictionary entries, converting any PropertyQuantity to float in SI units - """ - - d = {} - for k in obj.PropertiesList: - if obj.getTypeIdOfProperty(k) in QUANTITY_PROPERTIES: - q = Units.Quantity(getattr(obj, k)) - # q.Value is in FreeCAD internal units, which is same as SI except for mm instead of m - d[k] = q.Value/1000**q.Unit.Signature[0] - else: - d[k] = getattr(obj, k) - return d - - -def openFileManager(case_path): - case_path = os.path.abspath(case_path) - if platform.system() == 'MacOS': - subprocess.Popen(['open', '--', case_path]) - elif platform.system() == 'Linux': - subprocess.Popen(['xdg-open', case_path]) - elif platform.system() == 'Windows': - subprocess.Popen(['explorer', case_path]) - - -def writePatchToStl(solid_name, face_mesh, fid, scale=1): - fid.write("solid {}\n".format(solid_name)) - for face in face_mesh.Facets: - n = face.Normal - fid.write(" facet normal {} {} {}\n".format(n[0], n[1], n[2])) - fid.write(" outer loop\n") - for i in range(3): - p = [i * scale for i in face.Points[i]] - fid.write(" vertex {} {} {}\n".format(p[0], p[1], p[2])) - fid.write(" endloop\n") - fid.write(" endfacet\n") - fid.write("endsolid {}\n".format(solid_name)) - - -def enableLayoutRows(layout, selected_rows): - if isinstance(layout, QFormLayout): - for rowi in range(layout.count()): - for role in [QFormLayout.LabelRole, QFormLayout.FieldRole, QFormLayout.SpanningRole]: - item = layout.itemAt(rowi, role) - if item: - if isinstance(item, QtGui.QWidgetItem): - item.widget().setVisible(selected_rows is None or rowi in selected_rows) - elif isinstance(layout, QGridLayout): - for rowi in range(layout.rowCount()): - for coli in range(layout.columnCount()): - item = layout.itemAtPosition(rowi, coli) - if item: - if isinstance(item, QtGui.QWidgetItem): - item.widget().setVisible(selected_rows is None or rowi in selected_rows) - else: - for rowi in range(layout.count()): - item = layout.itemAt(rowi) - if item: - if isinstance(item, QtGui.QWidgetItem): - item.widget().setVisible(selected_rows is None or rowi in selected_rows) - - -def clearCase(case_path, backup_path=None): - """ - Remove and recreate contents of the case directory, optionally backing up - Does not remove the directory entry itself as this requires paraview to be reloaded - """ - if backup_path: - os.makedirs(backup_path) #mkdir -p - if os.path.isdir(case_path): - for entry in os.scandir(case_path): - if backup_path: - dest = os.path.join(backup_path, entry.name) - shutil.move(entry.path, dest) - else: - if entry.is_dir(): - shutil.rmtree(entry.path) - else: - os.remove(entry.path) - else: - os.makedirs(case_path) # mkdir -p - - -def executeMacro(macro_name): - macro_contents = "import FreeCAD\nimport FreeCADGui\nimport FreeCAD as App\nimport FreeCADGui as Gui\n" - macro_contents += open(macro_name).read() - exec(compile(macro_contents, macro_name, 'exec'), {'__file__': macro_name}) - - -class CfdSynchronousFoamProcess: - def __init__(self): - self.process = CfdConsoleProcess(stdout_hook=self.readOutput, stderr_hook=self.readError) - self.output = "" - self.outputErr = "" - self.outputAll = "" - - def run(self, cmdline, case=None): - if getFoamRuntime() == "PosixDocker" and ' pull ' not in cmdline: - if startDocker(): - return 1 - print("Running ", cmdline) - - mycmdline = makeRunCommand(cmdline,case) - print("cmdline in CfdSynFoamProcess:") - print(mycmdline) - - self.process.start(makeRunCommand(cmdline, case), env_vars=getRunEnvironment()) - if not self.process.waitForFinished(): - raise Exception("Unable to run command " + cmdline) - return self.process.exitCode() - - def readOutput(self, output): - self.output += output - self.outputAll += output - - def readError(self, output): - self.outputErr += output - self.outputAll += output - - - -# Only one container is needed. Start one for the CfdOF workbench as required -class DockerContainer: - container_id = None - usedocker = False - output_path_used = None - docker_cmd = None - - def __init__(self): - self.image_name = None - import shutil - - if shutil.which('podman') is not None: - self.docker_cmd = shutil.which('podman') - elif shutil.which('docker') is not None: - self.docker_cmd = shutil.which('docker') - else: - self.docker_cmd = None - - if self.docker_cmd is not None: - self.docker_cmd = self.docker_cmd.split(os.path.sep)[-1] - - def start_container(self): - prefs = getPreferencesLocation() - self.image_name = FreeCAD.ParamGet(prefs).GetString("DockerURL", "") - output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") - - if not output_path: - output_path = tempfile.gettempdir() - output_path = os.path.normpath(output_path) - - if not os.path.isdir(output_path): - print("Default output directory not found") - return 1 - - if DockerContainer.container_id != None: - print("Attempting to start container but id already set") - - container_ls = self.query_docker_container_ls() - if container_ls != None: - print("Docker container {} running but not started by CfdOF - it may not be configured correctly - stopping container.".format(container_ls)) - self.stop_container(alien = True) - - if self.image_name == "": - print("Docker image name not set") - return 1 - - if self.docker_cmd == None: - print("Need to install either podman or docker") - return 1 - - if platform.system() == 'Windows': - out_d = output_path.split(os.sep) - if len(out_d)>2 and out_d[2][:3] == 'wsl': - output_path = '/' + '/'.join(out_d[4:]) - - cmd = "{0} run -t -d -u 1000:1000 -v {1}:/tmp {2}".format(self.docker_cmd, output_path, self.image_name) - - if 'docker' in self.docker_cmd: - cmd = cmd.replace('docker.io/','') - proc = QtCore.QProcess() - proc.start(cmd) - self.getContainerID() - if self.container_id != None: - self.output_path_used = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") - return 0 - else: - return 1 - - """ Stop docker container and remove """ - def stop_container(self, alien = False): - if DockerContainer.container_id == None and alien: - DockerContainer.container_id = self.query_docker_container_ls() - if DockerContainer.container_id != None: - print("Stopping docker container {}".format(DockerContainer.container_id)) - cmd = "{} container rm -f {}".format(self.docker_cmd, DockerContainer.container_id) - proc = QtCore.QProcess() - proc.start(cmd) - if not proc.waitForFinished(): - print("Stop docker container {} failed".format(DockerContainer.container_id)) - else: - while proc.canReadLine(): - line = proc.readLine() - DockerContainer.container_id = None - DockerContainer.output_path_used = None - else: - print("No docker container to stop") - - def getContainerID(self): - import time - timer_counts = 0 - while timer_counts < 10 and DockerContainer.container_id == None: - time.sleep(1) - DockerContainer.container_id = self.query_docker_container_ls() - timer_counts = timer_counts + 1 - - def query_docker_container_ls(self): - cmd = "{} container ls".format(self.docker_cmd) - proc = QtCore.QProcess() - proc.start(cmd) - proc.waitForFinished() - while proc.canReadLine(): - line = proc.readLine() - cnt_lst = str(line.data(), encoding="utf-8").split() - if len(cnt_lst)>0 and cnt_lst[1].replace(':latest','').replace('docker.io/','') == self.image_name.replace(':latest','').replace('docker.io/',''): - return(cnt_lst[0]) - return(None) - From 1918c6650731f4ff0639efb527c3f91b464fd7a6 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 16 Mar 2023 23:04:03 -0600 Subject: [PATCH 09/20] Added remote host functionality --- CfdOF/CfdPreferencePage.py | 10 + CfdOF/CfdRemotePreferencePage.py | 1600 ++++++++++++++++++++++ CfdOF/CfdTools.py | 161 ++- CfdOF/Mesh/CfdMesh.py | 16 +- CfdOF/Mesh/CfdMeshTools.py | 53 +- CfdOF/Mesh/TaskPanelCfdMesh.py | 339 ++++- CfdOF/Solve/CfdCaseWriterFoam.py | 38 +- CfdOF/Solve/TaskPanelCfdSolverControl.py | 347 ++++- Gui/CfdPreferencePage.ui | 101 +- Gui/CfdRemotePreferencePage.ui | 856 ++++++++++++ Gui/TaskPanelCfdMesh.ui | 254 ++-- Gui/TaskPanelCfdSolverControl.ui | 219 +-- InitGui.py | 2 + 13 files changed, 3633 insertions(+), 363 deletions(-) create mode 100644 CfdOF/CfdRemotePreferencePage.py create mode 100644 Gui/CfdRemotePreferencePage.ui diff --git a/CfdOF/CfdPreferencePage.py b/CfdOF/CfdPreferencePage.py index d078672d..ac2e482a 100644 --- a/CfdOF/CfdPreferencePage.py +++ b/CfdOF/CfdPreferencePage.py @@ -114,6 +114,7 @@ def __init__(self): self.form.tb_choose_output_dir.clicked.connect(self.chooseOutputDir) self.form.le_output_dir.textChanged.connect(self.outputDirChanged) + self.form.cb_add_filename_to_output.clicked.connect(self.addFilenameChanged) self.form.cb_docker_sel.clicked.connect(self.dockerCheckboxClicked) self.form.pb_download_install_docker.clicked.connect(self.downloadInstallDocker) @@ -192,6 +193,11 @@ def loadSettings(self): self.setDownloadURLs() + if FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0): + self.form.cb_add_filename_to_output.setChecked(True) + else: + self.form.cb_add_filename_to_output.setChecked(False) + def consoleMessage(self, message="", colour_type=None): message = escape(message) message = message.replace('\n', '
') @@ -202,6 +208,10 @@ def consoleMessage(self, message="", colour_type=None): self.form.textEdit_Output.setText(self.console_message) self.form.textEdit_Output.moveCursor(QtGui.QTextCursor.End) + def addFilenameChanged(self): + prefs = CfdTools.getPreferencesLocation() + FreeCAD.ParamGet(prefs).SetBool("AddFilenameToOutput", self.form.cb_add_filename_to_output.isChecked()) + def foamDirChanged(self, text): self.foam_dir = text if self.foam_dir and os.access(self.foam_dir, os.R_OK): diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py new file mode 100644 index 00000000..339780d3 --- /dev/null +++ b/CfdOF/CfdRemotePreferencePage.py @@ -0,0 +1,1600 @@ + + +# *************************************************************************** +# * * +# * Copyright (c) 2017-2018 Oliver Oxtoby (CSIR) * +# * Copyright (c) 2017 Johan Heyns (CSIR) * +# * Copyright (c) 2017 Alfred Bogaers (CSIR) * +# * Copyright (c) 2019-2022 Oliver Oxtoby * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 3 of the * +# * License, or (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with this program. If not, * +# * see . * +# * * +# *************************************************************************** +# +# LinuxGuy123@gmail.com's notes. +# +# This file started out as a simple clone of the CfdPreferencesPage to handle a remote host instead +# of the local host. But with the changes needed to handle multiple host profiles, it changed a lot. +# +# The first iteration of this code was to change it to use a single remote host. +# +# The current iteration of this code now handles multiple remote hosts via host profiles. This resulted in +# the working parts getting almost complelely rewritten. This code presently only works with Linux remote hosts +# or at least it has only been tested with Linux remote hosts. I left in many pieces of the old code in case they are +# needed to add Docker, Windows, etc. on the remote host. +# +# How this page works... +# If Use Remote Processing is enabled, the controls are enabled. If not, they aren't. +# The page then loads the profile names from the parameters file. +# +# Then it loads the parameters for the first profile listed in the profile drop down box. +# With these parameters it initializes the controls and the local vars. +# When the users changes a parameter in the gui it is immediately written to the parameter file and +# the local var is updated. +# When the profile is changed the new parameters for the profile are loaded and the local vars +# are updated as are the controls. +# +# This page ignores the OK, Apply and Cancel buttons. There is a routine to save the current +# profile (saveProfile) on OK, change profile and Apply, but are those buttons are ignored in CfdOF. +# As they were in the previous version. Instead, every field is saved to the local var and the parameter file +# when it is entered. +# +# The remotePreferences GUI file (CfdRemotePreferencePage.ui) file still has many controls that are not used. See +# enableControls() for a list of what is or isn't used. +# They are set to disabled and not visible so they don't appear in the running GUI. They were left in in case +# they would be needed in the future. But generally the disabled controls don't make sense on a remote host - +# Paraview, for example. At some point, once the page has been deemed to operate in an acceptable manner, they +# should be removed entirely. +# +# In the initial code I wrote the variables that were used with remote operation had _remote_ in their name. I removed +# them because the var names were pretty long. Now all the vars in this code are assumed to be referring to a remote host. +# The only names that have remote in them are procedures that replaced non remote procedures. For example runRemoteDependencyCheck. +# I left the code for runDependencyCheck in the file because parts of it may be used if/when Windows or Docker is added for remote hosts. +# +# The reason I'm telling you this is to justify how messy the code is right now. It would be very easy to remove all the unused +# code and clean things up. But right now the unused code is present in case they are needed in the near future. +# +# This application doesn't install OpenFOAM on the remote host. It also doesn't install gmsh. It does install cfmesh +# if OpenFOAM (and OpenFOAM-devel) is installed. +# +# I did not use worker threads to install cfMesh. I just ran the processes from the ssh command line. +# +# TODO: +# - get cfMesh URL control working +# - get About Remote Processing document window working +# - put the remote host computer fields in a container like Docker, OpenFOAM, etc. +# - get tooltips working +# Done: implement host profiles so that multiple remote hosts can be used +# Done: store and load the use remote processing boolean +# Done: enable and disable all the controls with the cb_use_remote_processing checkbox result +# Done: store and load the host name and username fields +# Done: get ping host working +# Done: get test ssh working +# Nope: move ping and ssh to CfdOFTools ? +# Done: get remote foam dir storing and loading +# Done: remote gmsh path storing and loading +# Done: remote work dir storing and loading +# Done: get download and install cfMesh working + +#******************************************************************************** + +import os +import os.path +import platform +import sys +if sys.version_info >= (3,): # Python 3 + import urllib.request as urlrequest + import urllib.parse as urlparse +else: + import urllib as urlrequest + import urlparse + + #import traceback #This as used during debugging + +import ssl +import ctypes +import FreeCAD +from CfdOF import CfdTools +import tempfile +from contextlib import closing +from xml.sax.saxutils import escape + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore + from PySide import QtGui + from PySide.QtCore import Qt, QObject, QThread + from PySide.QtGui import QApplication + + +# Constants +OPENFOAM_URL = \ + "https://sourceforge.net/projects/openfoam/files/v2206/OpenFOAM-v2206-windows-mingw.exe/download" +OPENFOAM_FILE_EXT = ".exe" +PARAVIEW_URL = \ + "https://www.paraview.org/paraview-downloads/download.php?submit=Download&version=v5.10&type=binary&os=Windows&downloadFile=ParaView-5.10.1-Windows-Python3.9-msvc2017-AMD64.exe" +PARAVIEW_FILE_EXT = ".exe" +CFMESH_URL = \ + "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof.zip/download" +CFMESH_URL_MINGW = \ + "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof-binaries-{}.zip/download" +CFMESH_FILE_BASE = "cfmesh-cfdof" +CFMESH_FILE_EXT = ".zip" +HISA_URL = \ + "https://sourceforge.net/projects/hisa/files/hisa-master.zip/download" +HISA_URL_MINGW = \ + "https://sourceforge.net/projects/hisa/files/hisa-master-binaries-{}.zip/download" +HISA_FILE_BASE = "hisa-master" +HISA_FILE_EXT = ".zip" +DOCKER_URL = \ + "docker.io/mmcker/cfdof-openfoam" + +OPENFOAM_DIR = "/usr/lib/openfoam/openfoam2206" + + +# Tasks for the worker thread +# Not used anymore +DOWNLOAD_OPENFOAM = 1 +DOWNLOAD_PARAVIEW = 2 +DOWNLOAD_CFMESH = 3 +DOWNLOAD_HISA = 4 +DOWNLOAD_DOCKER = 5 + + +class CloseDetector(QObject): + def __init__(self, obj, callback): + super().__init__(obj) + self.callback = callback + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.ChildRemoved: + self.callback() + return False + + +#************************************************************************* + +class CfdRemotePreferencePage: + def __init__(self): + + # load the page + ui_path = os.path.join(CfdTools.getModulePath(), 'Gui', "CfdRemotePreferencePage.ui") + self.form = FreeCADGui.PySideUic.loadUi(ui_path) + + # set the preferences parameters location + self.prefs_location = CfdTools.getPreferencesLocation() + + # get the use remote processing parameter and set the use RP control + self.use_remote_processing = FreeCAD.ParamGet(self.prefs_location).GetBool("UseRemoteProcessing") + self.form.cb_use_remote_processing.setChecked(self.use_remote_processing) + + #enable controls appropriately + self.enableControls(self.use_remote_processing) + + # connect the handler for remote_processing.clicked + self.form.cb_use_remote_processing.clicked.connect(self.useRemoteProcessingChanged) + + #TODOself.form.cb_use_remote_processing.setToolTip("Remote processing is + # the use of a remote computer to run meshing and OpenFOAM operations") + + # set up the profiles combo box + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) + + # connect the hostname and username controls + self.form.le_hostname.textChanged.connect(self.hostnameChanged) + self.form.le_username.textChanged.connect(self.usernameChanged) + + #connect the ping and ssh test push buttons + self.form.pb_ping_host.clicked.connect(self.pingHost) + self.form.pb_test_ssh.clicked.connect(self.testSSH) + + #connect the mesh and foam processes and threads controls + self.form.le_mesh_threads.textChanged.connect(self.meshThreadsChanged) + self.form.le_mesh_processes.textChanged.connect(self.meshProcessesChanged) + self.form.le_foam_threads.textChanged.connect(self.foamThreadsChanged) + self.form.le_foam_processes.textChanged.connect(self.foamProcessesChanged) + + # connect the foam_dir changed control + #self.form.tb_choose_remote_foam_dir.clicked.connect(self.chooseFoamDir) + self.form.le_foam_dir.textChanged.connect(self.foamDirChanged) + + #connect the add filename to output control + self.form.cb_add_filename_to_output.clicked.connect(self.addFilenameToOutputChanged) + + + # connect handlers for various control events + # note: some of these aren't actually used. Code left in tact anyway + self.form.tb_choose_paraview_path.clicked.connect(self.chooseParaviewPath) + self.form.le_paraview_path.textChanged.connect(self.paraviewPathChanged) + + self.form.tb_choose_remote_gmsh_path.clicked.connect(self.chooseGmshPath) + self.form.le_gmsh_path.textChanged.connect(self.gmshPathChanged) + + self.form.pb_run_dependency_checker.clicked.connect(self.runRemoteDependencyChecker) + self.form.pb_download_install_openfoam.clicked.connect(self.downloadInstallOpenFoam) + self.form.tb_pick_openfoam_file.clicked.connect(self.pickOpenFoamFile) + self.form.pb_download_install_paraview.clicked.connect(self.downloadInstallParaview) + self.form.tb_pick_paraview_file.clicked.connect(self.pickParaviewFile) + + self.form.pb_download_install_cfMesh.clicked.connect(self.remoteDownloadInstallCfMesh) + self.form.tb_pick_cfmesh_file.clicked.connect(self.pickCfMeshFile) + self.form.pb_download_install_hisa.clicked.connect(self.downloadInstallHisa) + self.form.tb_pick_hisa_file.clicked.connect(self.pickHisaFile) + + self.form.pb_add_profile.clicked.connect(self.addProfile) + self.form.pb_delete_profile.clicked.connect(self.deleteProfile) + + # initialize general things with defaults + # this gets done from loadProfile() now. + # self.form.le_openfoam_url.setText(OPENFOAM_URL) + # self.form.le_paraview_url.setText(PARAVIEW_URL) + + self.form.tb_choose_remote_output_dir.clicked.connect(self.chooseOutputDir) + self.form.le_output_path.textChanged.connect(self.outputPathChanged) + + self.form.cb_docker_sel.clicked.connect(self.dockerCheckboxClicked) + self.form.pb_download_install_docker.clicked.connect(self.downloadInstallDocker) + + self.dockerCheckboxClicked() + + self.ev_filter = CloseDetector(self.form, self.cleanUp) + self.form.installEventFilter(self.ev_filter) + + # set global vars + self.thread = None + self.install_process = None + + self.console_message = "" + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + + self.profile_name = "" + self.host_prefs_location = "" + self.hostname = "" + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.gmsh_path = "" + self.add_filename_to_output = False + + # TODO:fix these references + # if they are still used. Most are not. + #self.initial_remote_foam_dir = "" + #self.paraview_path = "" + #self.initial_paraview_path = "" + #self.initial_remote_gmsh_path = "" + #self.remote_output_dir = "" + + # not sure what this is for + self.form.gb_openfoam.setVisible(platform.system() == 'Windows') + self.form.gb_paraview.setVisible(platform.system() == 'Windows') + + # now load the profiles into the combo box + self.loadProfileNames() + + #now load the top profile + self.loadProfile(self.form.cb_profile.currentText()) + + + # enable/disable the controls on this page + def enableControls(self,value): + + self.form.cb_profile.setEnabled(value) + self.form.pb_add_profile.setEnabled(value) + self.form.pb_delete_profile.setEnabled(value) + + self.form.le_hostname.setEnabled(value) + self.form.le_username.setEnabled(value) + self.form.pb_ping_host.setEnabled(value) + self.form.pb_test_ssh.setEnabled(value) + + self.form.le_mesh_processes.setEnabled(value) + self.form.le_mesh_threads.setEnabled(value) + self.form.le_foam_processes.setEnabled(value) + self.form.le_foam_threads.setEnabled(value) + + self.form.le_foam_dir.setEnabled(value) + self.form.le_output_path.setEnabled(value) + self.form.cb_add_filename_to_output.setEnabled(value) + + self.form.pb_run_dependency_checker.setEnabled(value) + + self.form.pb_download_install_cfMesh.setEnabled(value) + self.form.le_cfmesh_url.setEnabled(value) + + + # Controls below here are not yet operational regardless of if remote processing + # is enabled or not. So they are disabled. Change this as new functionality + # is added to the page + value = False + + self.form.pb_about_remote_processing.setEnabled(value) + + #disable the foam path chooser + self.form.tb_choose_remote_foam_dir.setEnabled(value) + self.form.tb_choose_remote_foam_dir.setVisible(value) + + # disable paraview stuff and make it invisible because we + # don't run paraview on the remote computer + # This makes it disappear from the page but allows us to + # leave the controls and code in place + self.form.le_paraview_path.setEnabled(value) + self.form.le_paraview_path.setVisible(False) + self.form.lbl_remote_paraview_path.setVisible(False) + self.form.tb_choose_paraview_path.setEnabled(value) + self.form.tb_choose_paraview_path.setVisible(False) + + # disable the gmsh path + # we don't need to know gmsh's path in Linux. Either it is installed or not + # we just call it from the shell, This might change if the remote host is running + # a different OS. + self.form.lbl_gmsh_path.setVisible(False) + self.form.le_gmsh_path.setEnabled(value) + self.form.le_gmsh_path.setVisible(False) + self.form.tb_choose_remote_gmsh_path.setEnabled(value) + self.form.tb_choose_remote_gmsh_path.setVisible(value) + + #disable the output path chooser + self.form.tb_choose_remote_output_dir.setEnabled(value) + self.form.tb_choose_remote_output_dir.setVisible(False) + + + # disable docker stuff. Setting it to invisible does not + # make it disappear. + self.form.gb_docker.setEnabled(False) + self.form.gb_docker.setVisible(False) + + self.form.pb_download_install_openfoam.setEnabled(value) + self.form.tb_pick_openfoam_file.setEnabled(value) + + self.form.pb_download_install_paraview.setEnabled(value) + self.form.tb_pick_paraview_file.setEnabled(value) + + self.form.cb_docker_sel.setEnabled(value) + self.form.le_docker_url.setEnabled(value) + self.form.pb_download_install_docker.setEnabled(value) + + self.form.tb_pick_cfmesh_file.setEnabled(value) + #self.form.pb_download_install_cfMesh.setEnabled(value) + + self.form.pb_download_install_hisa.setEnabled(value) + self.form.tb_pick_hisa_file.setEnabled(value) + self.form.le_hisa_url.setEnabled(value) + self.form.pb_download_install_hisa.setEnabled(value) + + self.form.le_openfoam_url.setEnabled(value) + + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + self.username = "" + self.hostname = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.add_filename_to_output = False + + else: + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + + #now set the control values + self.form.le_hostname.setText(self.hostname) + self.form.le_username.setText(self.username) + self.form.le_mesh_processes.setText(str(self.mesh_processes)) + self.form.le_mesh_threads.setText(str(self.mesh_threads)) + self.form.le_foam_processes.setText(str(self.foam_processes)) + self.form.le_foam_threads.setText(str(self.foam_threads)) + self.form.le_foam_dir.setText(self.foam_dir) + self.form.le_output_path.setText(self.output_path) + self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + + + # create a new profile and add it to the cb_profile control + # and create its entry in the CfdOF parameters + def addProfile(self): + # get the new profile name + #https://wiki.freecad.org/PySide_Beginner_Examples/en + profile_name, ok = QtGui.QInputDialog.getText(None,"Profile Name", "Enter the name of the profile you'd like to add.\n It cannot contain spaces or slashes") + if ok and profile_name: + print("Adding: " + profile_name) + #TODO: test for spaces or slashes here. + + # add the profile name to profile combo list + self.form.cb_profile.addItem(profile_name) + # move to the item just added + # TODO: check if text isn't found, ie index = -1 + index = self.form.cb_profile.findText(profile_name) + self.form.cb_profile.setCurrentIndex(index) + + # update the global profile_name var + self.profile_name = profile_name + + # update the global host_prefs_location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + # add the default setting for a profile into parameters + hostPrefs = self.host_prefs_location + FreeCAD.ParamGet(hostPrefs).SetString("Hostname", "") + FreeCAD.ParamGet(hostPrefs).SetString("Username", "") + FreeCAD.ParamGet(hostPrefs).SetInt("MeshProcesses", 0) + FreeCAD.ParamGet(hostPrefs).SetInt("MeshThreads", 0) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamProcesses", 0) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamThreads", 0) + FreeCAD.ParamGet(hostPrefs).SetString("FoamDir", OPENFOAM_DIR) + FreeCAD.ParamGet(hostPrefs).SetString("OutputPath","/tmp") + FreeCAD.ParamGet(hostPrefs).SetBool("AddFilenameToOutput", False) + + # now load the controls and local vars from the profile parameters + self.loadProfile(profile_name) + + # delete the current profile in the cb_profile and in parameters + # this will trigger a profile change which will update the controls and + # vars with the values from the new profile + def deleteProfile(self): + #check if there is a profile in the profile combo box to delete + if self.form.cb_profile.currentText() == "": + return + + # ask the user if they really want to delete the profile + #https://wiki.freecad.org/PySide_Beginner_Examples/en + deleteProfile = self.form.cb_profile.currentText() + deleteIndex = self.form.cb_profile.currentIndex() + deleteQuestion = "Delete profile " + deleteProfile + "?\n" + "This cannot be undone." + + reply = QtGui.QMessageBox.question(None, "Delete profile ?", deleteQuestion, + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + + if reply == QtGui.QMessageBox.Yes: + #delete it from the control + self.form.cb_profile.removeItem(deleteIndex) + # delete it from the parameters list + profileDir = self.prefs_location + "/Hosts" + FreeCAD.ParamGet(profileDir).RemGroup(deleteProfile) + + if reply == QtGui.QMessageBox.No: + # this is where the code relevant to a 'No' answer goes + pass + + # this gets called when the user changes the profile + # Note: doesn't save the last profile before changing to the new profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + + + # save the current profile by writing its the parameters + def saveProfile(self): + print("saveProfile has fired") + if self.profile_name != "": + hostPrefs = self.host_prefs_location + FreeCAD.ParamGet(hostPrefs).SetString("Hostname", self.hostname) + FreeCAD.ParamGet(hostPrefs).SetString("Username", self.username) + FreeCAD.ParamGet(hostPrefs).SetInt("MeshProcesses", self.mesh_processes) + FreeCAD.ParamGet(hostPrefs).SetInt("MeshThreads", self.mesh_threads) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamProcesses",self.foam_processes) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamThreads",self.foam_threads) + FreeCAD.ParamGet(hostPrefs).SetString("FoamDir", self.foam_dir) + FreeCAD.ParamGet(hostPrefs).SetString("OutputPath",self.output_path) + FreeCAD.ParamGet(hostPrefs).SetBool("AddFilenameToOutput", self.add_filename_to_output) + self.profileChanged = False + + def meshThreadsChanged(self): + if self.profile_name == "": + return + if self.form.le_mesh_threads.text() == "": + return + #else + #update the global var + self.mesh_threads = int(self.form.le_mesh_threads.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("MeshThreads", self.mesh_threads) + + def meshProcessesChanged(self): + if self.profile_name == "": + return + if self.form.le_mesh_processes.text() == "": + return + #else + #update the global var + self.mesh_processes = int(self.form.le_mesh_processes.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("MeshProcesses", self.mesh_processes) + + def foamThreadsChanged(self): + if self.profile_name == "": + return + if self.form.le_foam_threads.text() == "": + return + #else + #update the global var + self.foam_threads = int(self.form.le_foam_threads.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("FoamThreads", self.foam_threads) + + def foamProcessesChanged(self): + #print("foamProcessesChanged has fired") + #traceback.print_stack() + if self.profile_name == "": + return + if self.form.le_foam_processes.text() == "": + return + #else + #update the global var + self.foam_processes = int(self.form.le_foam_processes.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("FoamProcesses", self.foam_processes) + + def useRemoteProcessingChanged(self): + if (self.form.cb_use_remote_processing.isChecked() == True): + self.enableControls(True) + FreeCAD.ParamGet(self.prefs_location).SetBool("UseRemoteProcessing", True) + else: + self.enableControls(False) + FreeCAD.ParamGet(self.prefs_location).SetBool("UseRemoteProcessing", False) + + def hostnameChanged(self): + if self.profile_name == "": + return + # update the global variables + self.hostname = self.form.le_hostname.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("Hostname", self.hostname ) + self.profile_changed = True + + def usernameChanged(self): + if self.profile_name == "": + return + # update the global variables + self.username = self.form.le_username.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("Username", self.username ) + self.profile_changed = True + + def foamDirChanged(self): + #TODO: check that the path doesn't end with "/" + if self.profile_name == "": + return + # update the global variables + self.foam_dir = self.form.le_foam_dir.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("FoamDir", self.foam_dir ) + self.profile_changed = True + """ + self.foam_dir = text + if self.foam_dir and os.access(self.foam_dir, os.R_OK): + #TODO: change this? + #self.setDownloadURLs() + """ + def gmshPathChanged(self): + #TODO: check that the path doesn't end in "/" + if self.profile_name == "": + return + self.gmsh_path = self.form.le_gmsh_path.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("GmshDir", self.foam_dir ) + self.profile_changed = True + + def outputPathChanged(self, text): + #TODO: check that the path doesn't end in "/" + if self.profile_name == "": + return + self.output_path = self.form.le_output_path.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("OutputPath", self.output_path ) + self.profile_changed = True + + def addFilenameToOutputChanged(self): + #TODO: check that the filename doesn't have a space in it + if self.profile_name == "": + return + self.add_filename_to_output = self.form.cb_add_filename_to_output.isChecked() + FreeCAD.ParamGet(self.host_prefs_location).SetBool("AddFilenameToOutput", self.add_filename_to_output) + self.profile_changed = True + + #TODO: Move this to CfdTools + def pingHost(self): + self.consoleMessage("Performing ping test...") + if self.hostname == "": + self.consoleMessage("Error: missing hostname or IP address.") + return + result = CfdTools.runCommand("ping",["-c8", self.hostname]) + #result = runCommand("ssh", ["me@192.168.2.159", "ls", "-al"]) + #result = runCommand("ssh", ["me@192.168.2.159", "ping", "-c8","www.google.com"]) + #result = runCommand("ssh", ["me@192.168.2.159", "uname", "-a"]) + if result == 0: + self.consoleMessage("Successfully pinged " + self.hostname + ".") #+ remote_hostname +".") + else: + self.consoleMessage("Could not ping the remote host. Check network configure and test manually.") + + #TODO: Move this to CfdTools + def testSSH(self): + self.consoleMessage("Performing ssh test...") + if self.hostname == "": + self.consoleMessage("Error: missing hostname or IP address.") + return + if self.username =="": + self.consoleMessage("Error: missing username.") + return + + connectString = self.form.le_username.text() + "@" + self.hostname + #print("ssh connectString:", connectString); + result = CfdTools.runCommand("ssh",[connectString,"ls -al"]) + if result == 0: + self.consoleMessage("Successfully ran an ssh session on " + self.hostname + ".") + else: + self.consoleMessage("Failed to establish an ssh session with the remote host. Manually verify ssh operation with the remote host.") + + + def __del__(self): + self.cleanUp() + + + def cleanUp(self): + if self.thread and self.thread.isRunning(): + FreeCAD.Console.PrintError("Terminating a pending install task\n") + self.thread.quit = True + if self.install_process and self.install_process.state() == QtCore.QProcess.Running: + FreeCAD.Console.PrintError("Terminating a pending install task\n") + self.install_process.terminate() + QApplication.restoreOverrideCursor() + + # This gets called from outside the page + def saveSettings(self): + pass + #self.saveProfile() <- this is called in _init_ Doesn't need to be called here. + """ + print("Error: saveSettings has been depreciated.") + CfdTools.setRemoteFoamDir(self.foam_dir) + CfdTools.setParaviewPath(self.paraview_path) + CfdTools.setRemoteGmshPath(self.gmsh_path) + prefs = self.prefs_location + FreeCAD.ParamGet(prefs).SetString("RemoteOutputPath", self.remote_output_dir) + FreeCAD.ParamGet(prefs).SetBool("UseDocker",self.form.cb_docker_sel.isChecked()) + FreeCAD.ParamGet(prefs).SetString("DockerURL",self.form.le_docker_url.text()) + """ + + # This gets called form outside the page + def loadSettings(self): + pass + #self.loadProfile(self.profile_name) + """ + print("Error: loadSettings has been depreciated.") + # Don't set the autodetected location, since the user might want to allow that to vary according + # to WM_PROJECT_DIR setting + prefs = self.prefs_location + self.foam_dir = FreeCAD.ParamGet(prefs).GetString("RemoteInstallationPath", "") + self.initial_remote_foam_dir = str(self.foam_dir) + self.form.le_foam_dir.setText(self.foam_dir) + + # Added by me + self.remote_output_dir = FreeCAD.ParamGet(prefs).GetString("RemoteOutputPath", "") + self.initial_remote_output_dir = str(self.remote_output_dir) + self.form.le_output_path.setText(self.remote_output_dir) + + self.paraview_path = CfdTools.getParaviewPath() + self.initial_paraview_path = str(self.paraview_path) + self.form.le_paraview_path.setText(self.paraview_path) + + self.gmsh_path = CfdTools.getRemoteGmshPath() + self.initial_remote_gmsh_path = str(self.gmsh_path) + self.form.le_gmsh_path.setText(self.gmsh_path) + + self.output_dir = CfdTools.getDefaultRemoteOutputPath() + self.form.le_output_path.setText(self.remote_output_dir) + + # Load the use_remote_processing parameter + if FreeCAD.ParamGet(prefs).GetBool("UseRemoteProcessing", 0): + self.form.cb_use_remote_processing.setCheckState(Qt.Checked) + self.enableControls(True) + else: + self.enableControls(False) + + #load the hostname + self.form.le_hostname.setText(FreeCAD.ParamGet(prefs).GetString("RemoteHostname","")) + self.form.le_username.setText(FreeCAD.ParamGet(prefs).GetString("RemoteUsername","")) + + if FreeCAD.ParamGet(prefs).GetBool("UseDocker", 0): + self.form.cb_docker_sel.setCheckState(Qt.Checked) + + # Set usedocker and enable/disable download buttons + self.dockerCheckboxClicked() + + self.form.le_docker_url.setText(FreeCAD.ParamGet(prefs).GetString("DockerURL", DOCKER_URL)) + + self.setDownloadURLs() + """ + + + def consoleMessage(self, message="", colour_type=None): + message = escape(message) + message = message.replace('\n', '
') + if colour_type: + self.console_message += '{1}
'.format(CfdTools.getColour(colour_type), message) + else: + self.console_message += message+'
' + self.form.textEdit_Output.setText(self.console_message) + self.form.textEdit_Output.moveCursor(QtGui.QTextCursor.End) + + + # Not used anymore + def testGetRuntime(self, disable_exception=True): + print("Error:testGetRuntime has been depreciated.") + """ Set the foam dir temporarily and see if we can detect the runtime """ + CfdTools.setFoamDir(self.foam_dir) + try: + runtime = CfdTools.getFoamRuntime() + except IOError as e: + runtime = None + if not disable_exception: + raise + CfdTools.setFoamDir(self.initial_remote_foam_dir) + return runtime + + + def setDownloadURLs(self): + print("Error:setDownloadURLs has been depreciated.") + if self.testGetRuntime() == "MinGW": + # Temporarily apply the foam dir selection + CfdTools.setFoamDir(self.foam_dir) + foam_ver = os.path.split(CfdTools.getFoamDir())[-1] + self.form.le_cfmesh_url.setText(CFMESH_URL_MINGW.format(foam_ver)) + self.form.le_hisa_url.setText(HISA_URL_MINGW.format(foam_ver)) + CfdTools.setFoamDir(self.initial_remote_foam_dir) + else: + self.form.le_cfmesh_url.setText(CFMESH_URL) + self.form.le_hisa_url.setText(HISA_URL) + + def paraviewPathChanged(self, text): + print("Error:paraviewPathChanged has been depreciated.") + self.paraview_path = text + + def chooseFoamDir(self): + d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose OpenFOAM directory', self.foam_dir) + if d and os.access(d, os.R_OK): + self.foam_dir = d + self.form.le_foam_dir.setText(self.foam_dir) + + def chooseParaviewPath(self): + p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView executable', self.paraview_path, + filter="*.exe" if platform.system() == 'Windows' else None) + if p and os.access(p, os.R_OK): + self.paraview_path = p + self.form.le_paraview_path.setText(self.paraview_path) + + def chooseGmshPath(self): + p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose gmsh executable', self.gmsh_path, + filter="*.exe" if platform.system() == 'Windows' else None) + if p and os.access(p, os.R_OK): + self.gmsh_path = p + self.form.le_gmsh_path.setText(self.gmsh_path) + + def chooseOutputDir(self): + d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose output directory', self.output_dir) + if d and os.access(d, os.W_OK): + self.output_dir = os.path.abspath(d) + self.form.le_output_path.setText(self.output_dir) + + + #******************************************************************************************* + # This function assumes the remote computer is running Linux or a derivative and a vanilla + # OpenFOAM installation. It does not handle a Windows remote computer or Docker at this time. + # It also does simple checks of the provided paths. It does not attempt to find executables the + # way the non remote dependency checker does + # It does not attempt to find ParaView on the remote computer because ParaView is not used as a server in + # FreeCAD. Though it could be, some day. + + def checkRemoteCfdDependencies(self): + + CF_MAJOR_VER_REQUIRED = 1 + CF_MINOR_VER_REQUIRED = 16 + + HISA_MAJOR_VER_REQUIRED = 1 + HISA_MINOR_VER_REQUIRED = 6 + HISA_PATCH_VER_REQUIRED = 4 + + return_message = "" + FreeCAD.Console.PrintMessage("Checking remote host CFD dependencies on " + self.hostname + "...\n") + + remote_user = self.username + remote_hostname = self.hostname + ssh_prefix = "ssh " + remote_user + "@" + remote_hostname + " " + + # check that the remote host is running Linux + # right now Linux is the only OS supported on the remote host + print("Checking the operating system on the remote host...\n") + + try: + os_string = CfdTools.runFoamCommand(ssh_prefix + "uname -o")[0] + except: + return_message += "Could not determine the OS on " + self.hostname + ".\n" + else: + if os_string.find("Linux") != 0: + return_message += "Remote host OS is " + os_string[:-1] + ".\n" + + else: + return_message += "The remote host OS is not Linux.\n" + return_message += "CfdOF requires that the remote host be running Linux.\n" + + # check that the open foam directory exists on the remote host + print("Checking the OpenFOAM directory...") + remote_foam_dir = self.foam_dir + foam_ls_command = ssh_prefix + " ls " + remote_foam_dir + #print("OpenFOAM dir testCommand:", foam_ls_command) + + try: + CfdTools.runFoamCommand(foam_ls_command) + except: + return_message += "Failed to find the OpenFOAM directory: "+ remote_foam_dir + " on " + self.hostname +".\n" + # print the message on the console + else: + return_message += "Found OpenFOAM directory " + remote_foam_dir + " on " + self.hostname + ".\n" + + + """ + try: + foam_dir = getRemoteFoamDir() + sys_msg = "System: {}\nRuntime: {}\nOpenFOAM directory: {}".format( + platform.system(), getFoamRuntime(), foam_dir if len(foam_dir) else "(system installation)") + print(sys_msg) + message += sys_msg + '\n' + except IOError as e: + ofmsg = "Could not find OpenFOAM installation: " + str(e) + print(ofmsg) + message += ofmsg + '\n' + else: + if foam_dir is None: + ofmsg = "OpenFOAM installation path not set and OpenFOAM environment neither pre-loaded before " + \ + "running FreeCAD nor detected in standard locations" + print(ofmsg) + message += ofmsg + '\n' + else: + if getFoamRuntime() == "PosixDocker": + startDocker() + try: + if getFoamRuntime() == "MinGW": + foam_ver = runFoamCommand("echo $FOAM_API")[0] + else: + foam_ver = runFoamCommand("echo $WM_PROJECT_VERSION")[0] + """ + + # Check if OpenFOAM will run and what its version is + FreeCAD.Console.PrintMessage("Checking OpenFOAM operation and version... ") + + try: + # TODO: figure out why foamVersion won't run properly in an ssh statement + # need sleep in the command so that the OpenFOAM source script gets run in the shell before foamVersion does + # help_string = runFoamCommand(ssh_prefix +"sleep 4s ; foamVersion")[0] + + help_string = CfdTools.runFoamCommand(ssh_prefix + "simpleFoam -help")[0] + except Exception as e: + runmsg = "Unable to run OpenFOAM: " + str(e) + return_message += runmsg + '\n' + print(runmsg) + #raise + else: + # TODO: this should be put in a try to go with the except below in case + # there is an issue with parsing out the OF version + # The command $foamVersion will give the version as well + version_start = help_string.find("OpenFOAM-") + 9 + version_string = help_string[version_start:version_start+4] + print(version_string) + return_message += "Successfully ran OpenFOAM version " + version_string + " on " + self.hostname + ".\n" + foam_version = int(version_string) + + """ + foam_ver = foam_ver.rstrip() + if foam_ver: + foam_ver = foam_ver.split()[-1] + if foam_ver and foam_ver != 'dev' and foam_ver != 'plus': + try: + # Isolate major version number + foam_ver = foam_ver.lstrip('v') + foam_ver = int(foam_ver.split('.')[0]) + if getFoamRuntime() == "MinGW": + if foam_ver < 2012 or foam_ver > 2206: + vermsg = "OpenFOAM version " + str(foam_ver) + \ + " is not currently supported with MinGW installation" + message += vermsg + "\n" + print(vermsg) + """ + if foam_version >= 1000: # Plus version + if foam_version < 1706: + vermsg = "OpenFOAM version " + str(foam_version) + " is outdated:\n" + \ + "Minimum version 1706 or 5 required" + return_message += vermsg + "\n" + print(vermsg) + if foam_version > 2206: + vermsg = "OpenFOAM version " + str(foam_version) + " is not yet supported:\n" + \ + "Last tested version is 2206" + return_message += vermsg + "\n" + print(vermsg) + """ + This code does not support foundation version numbers at this time. + TODO: handle foundation version numbers + else: # Foundation version + if foam_ver < 5: + vermsg = "OpenFOAM version " + str(foam_ver) + " is outdated:\n" + \ + "Minimum version 5 or 1706 required" + message += vermsg + "\n" + print(vermsg) + if foam_ver > 9: + vermsg = "OpenFOAM version " + str(foam_ver) + " is not yet supported:\n" + \ + "Last tested version is 9" + message += vermsg + "\n" + print(vermsg) + except ValueError: + vermsg = "Error parsing OpenFOAM version string " + foam_ver + message += vermsg + "\n" + print(vermsg) + + # Check for wmake + # if getFoamRuntime() != "MinGW" and getFoamRuntime() != "PosixDocker": + """ + + try: + # changed this from -help to -version + wmake_reply = CfdTools.runFoamCommand(ssh_prefix + "wmake -version")[0] + + except Exception as e: + #print(e) + wmakemsg = "Cannot run 'wmake' on remote host. \n" + wmakemsg += "Installation of cfMesh and HiSA is not be possible without 'wmake'.\n" + wmakemsg += "'wmake' is installed with most OpenFOAM development packages.\n" + wmakemsg += "You may want to install the OpenFOAM development package on the remote host to provide 'wmake'.\n" + return_message += wmakemsg + print(wmakemsg) + + else: + print(wmake_reply) + return_message += "'wmake' is installed on " + self.hostname + ".\n" + + # Check for cfMesh + try: + cfmesh_ver = CfdTools.runFoamCommand(ssh_prefix + "cartesianMesh -version")[0] + version_line = cfmesh_ver.splitlines()[-1] + vermsg = version_line[:-1] + " found on " + self.hostname + "." + cfmesh_ver = cfmesh_ver.rstrip().split()[-1] + cfmesh_ver = cfmesh_ver.split('.') + if (not cfmesh_ver or len(cfmesh_ver) != 2 or + int(cfmesh_ver[0]) < CF_MAJOR_VER_REQUIRED or + (int(cfmesh_ver[0]) == CF_MAJOR_VER_REQUIRED and + int(cfmesh_ver[1]) < CF_MINOR_VER_REQUIRED)): + vermsg += "cfMesh-CfdOF version {}.{} required".format(CF_MAJOR_VER_REQUIRED, CF_MINOR_VER_REQUIRED) + + except Exception as e: #subprocess.CalledProcessError: + vermsg = "cfMesh (CfdOF version) not found on " + self.hostname + "." + return_message += vermsg + '\n' + print(vermsg) + + + # Check for HiSA + try: + hisa_ver = CfdTools.runFoamCommand(ssh_prefix + "hisa -version")[0] + hisa_ver = hisa_ver.rstrip().split()[-1] + hisa_ver = hisa_ver.split('.') + if (not hisa_ver or len(hisa_ver) != 3 or + int(hisa_ver[0]) < HISA_MAJOR_VER_REQUIRED or + (int(hisa_ver[0]) == HISA_MAJOR_VER_REQUIRED and + (int(hisa_ver[1]) < HISA_MINOR_VER_REQUIRED or + (int(hisa_ver[1]) == HISA_MINOR_VER_REQUIRED and + int(hisa_ver[2]) < HISA_PATCH_VER_REQUIRED)))): + vermsg = "HiSA version {}.{}.{} required".format(HISA_MAJOR_VER_REQUIRED, + HISA_MINOR_VER_REQUIRED, + HISA_PATCH_VER_REQUIRED) + return_message += vermsg + "\n" + print(vermsg) + except Exception as e: #subprocess.CalledProcessError: + hisa_msg = "HiSA not found on " + self.hostname + "." + return_message += hisa_msg + '\n' + print(hisa_msg) + + + print("Checking for gmsh:") + # check that gmsh version 2.13 or greater is installed + # Note: ssh runs things in a shell. As long as the environment vars for that shell + # are set up properly on the remote computer, one doesn't need to know the gmsh path. + # You just call it and the shell $PATH takes care of the rest. + + try: + gmsh_reply = CfdTools.runFoamCommand(ssh_prefix + "gmsh -version")[0] + + except Exception as e: + #print(e) + gmsh_msg = "Cannot run 'gmsh' on " + self.hostname + ". \n" + gmsh_msg += "Please install gmsh on the remote host." + return_message += gmsh_msg + print(gmsh_msg) + + else: + # print(gmsh_reply) + # print(type(gmsh_reply)) + return_message += "gmsh version " + gmsh_reply[:-1] + " is installed on " + self.hostname + ".\n" + versionlist = gmsh_reply.split(".") + if int(versionlist[0]) < 2 or (int(versionlist[0]) == 2 and int(versionlist[1]) < 13): + gmsh_ver_msg = "The installed gmsh version is older than minimum required (2.13).\n" + gmsh_ver_msg += "Please update gmsh on the remote host." + return_message += gmsh_ver_msg + '\n' + print(gmsh_ver_msg) + + """ + gmshversion = "" + gmsh_exe = getRemoteGmshExecutable() + if gmsh_exe is None: + gmsh_msg = "gmsh not found (optional)" + return_message += gmsh_msg + '\n' + print(gmsh_msg) + else: + gmsh_msg = "gmsh executable: " + gmsh_exe + return_message += gmsh_msg + '\n' + print(gmsh_msg) + try: + # Needs to be runnable from OpenFOAM environment + gmshversion = runFoamCommand("'" + gmsh_exe + "'" + " -version")[2] + except (OSError, subprocess.CalledProcessError): + gmsh_msg = "gmsh could not be run from OpenFOAM environment" + return_message += gmsh_msg + '\n' + print(gmsh_msg) + if len(gmshversion) > 1: + # Only the last line contains gmsh version number + gmshversion = gmshversion.rstrip().split() + gmshversion = gmshversion[-1] + versionlist = gmshversion.split(".") + if int(versionlist[0]) < 2 or (int(versionlist[0]) == 2 and int(versionlist[1]) < 13): + gmsh_ver_msg = "gmsh version is older than minimum required (2.13)" + return_message += gmsh_ver_msg + '\n' + print(gmsh_ver_msg) + """ + + print("Completed CFD remote dependency check.") + return_message += "Completed remote host dependency check on " + self.hostname + ".\n" + return return_message + + + # This dependency checker runs on the remote host + def runRemoteDependencyChecker(self): + + if self.profile_name == "": + self.consoleMessage("Error: empty profile") + return + + if self.hostname == "": + self.consoleMessage("Error: missing hostname or IP address") + return + + if self.username == "": + self.consoleMessage("Error: missing username") + return + + remote_hostname = self.hostname + + # Temporarily apply the foam dir selection and paraview path selection + #CfdTools.setRemoteFoamDir(self.foam_dir) + #CfdTools.setParaviewPath(self.paraview_path) + #CfdTools.setRemoteGmshPath(self.gmsh_path) + #QApplication.setOverrideCursor(Qt.WaitCursor) + + self.consoleMessage("Checking dependencies on " + remote_hostname + "...") + msg = self.checkRemoteCfdDependencies() + self.consoleMessage(msg) + #CfdTools.setRemoteFoamDir(self.initial_remote_foam_dir) + #CfdTools.setParaviewPath(self.initial_paraview_path) + #CfdTools.setRemoteGmshPath(self.initial_remote_gmsh_path) + QApplication.restoreOverrideCursor() + + + # Not used anymore. Kept for reference sake + # Some of the vars may have been changed to their remote versions + """ + def runDependencyChecker(self): + # Temporarily apply the foam dir selection and paraview path selection + CfdTools.setFoamDir(self.foam_dir) + CfdTools.setParaviewPath(self.paraview_path) + CfdTools.setGmshPath(self.gmsh_path) + QApplication.setOverrideCursor(Qt.WaitCursor) + self.consoleMessage("Checking dependencies...") + msg = CfdTools.checkCfdDependencies() + self.consoleMessage(msg) + CfdTools.setFoamDir(self.initial_foam_dir) + CfdTools.setParaviewPath(self.initial_paraview_path) + CfdTools.setGmshPath(self.initial_gmsh_path) + QApplication.restoreOverrideCursor() + """ + + + def showAdministratorWarningMessage(self): + if platform.system() == "Windows": + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + if not is_admin: + button = QtGui.QMessageBox.question( + None, + "CfdOF Workbench", + "Before installing this software, it is advised to run FreeCAD in administrator mode (hold down " + " the 'Shift' key, right-click on the FreeCAD launcher, and choose 'Run as administrator').\n\n" + "If this is not possible, please make sure OpenFOAM is installed in a location to which you have " + "full read/write access rights.\n\n" + "You are not currently running as administrator - do you wish to continue anyway?") + return button == QtGui.QMessageBox.StandardButton.Yes + return True + + def downloadInstallOpenFoam(self): + if not self.showAdministratorWarningMessage(): + return + if self.createThread(): + self.thread.task = DOWNLOAD_OPENFOAM + self.thread.openfoam_url = self.form.le_openfoam_url.text() + self.thread.start() + + def pickOpenFoamFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose OpenFOAM install file', filter="*.exe") + if f and os.access(f, os.R_OK): + self.form.le_openfoam_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallParaview(self): + if self.createThread(): + self.thread.task = DOWNLOAD_PARAVIEW + self.thread.paraview_url = self.form.le_paraview_url.text() + self.thread.start() + + def pickParaviewFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView install file', filter="*.exe") + if f and os.access(f, os.R_OK): + self.form.le_paraview_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + + #********************************************************************************************** + # TODO: Update to use profiles + def remoteDownloadInstallCfMesh(self): + + #TODO: this routine doesn't clean up after itself if it fails + # Nor does it check for a full or partial build + # If the user reruns the build after fully or partially building previously + # this routine will fail. + + # Get the username and hostname for the remote host + + remote_user = self.username + remote_hostname = self.hostname + ssh_prefix = "ssh -tt " + remote_user + "@" + remote_hostname + " " + working_dir = self.output_path + + self.consoleMessage("Installing cfMesh on " + self.hostname + "...") + + # create a dir to download the CfMesh source files to + # cfMesh is installed in the user's home directory, not the output directory + + command = "EOT\n" + #command += "cd " + working_dir + "\n" + command += "mkdir cfMesh" + "\n" + command += "exit \n" + command += "EOT" + + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + "<< " + command)[0] + working_dir += "/cfMesh" + + print("Created cfMesh install dir: " + working_dir) + self.consoleMessage("Created cfMesh install directory " + working_dir) + except: + self.consoleMessage("Could not create an install directory on " + self.hostname + ".") + return + + # download the cfMesh source files + command = "EOT\n" + command += "cd " + working_dir + "\n" + command += "wget " + self.form.le_cfmesh_url.text() + "\n" + command += "exit \n" + command += "EOT" + + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + " << " + command)[0] + self.consoleMessage("Downloaded the cfMesh source files") + except: + self.consoleMessage("Could not download the cfMesh source files.") + return + + # unzip the downloaded file + # TODO: this assumes the downloaded archive is called "download". Which might not always be the case. + # Get name of the downloaded file from the URL and use that filename in this command + command = "EOT\n" + command += "cd " + working_dir + "\n" + command += "unzip download \n" + command += "exit \n" + command += "EOT" + + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + " << " + command)[0] + self.consoleMessage("Unzipped cfMesh source files") + except: + self.consoleMessage("Could not unzip cfMesh source files.") + return + + # run the wmake process + # TODO: add the number of processes to use on the build with export WM_NCOMPPROCS=`nproc`; + # TODO: add logging wiht ./Allwmake -> log.Allwmake + # TODO: delete the previous log before starting the new build + # Executing: { rm -f log.Allwmake; export WM_NCOMPPROCS=`nproc`; ./Allwmake 1> >(tee -a log.Allwmake) 2> >(tee -a log.Allwmake >&2); } in $WM_PROJECT_USER_DIR/cfmesh-cfdof + + command = "EOT\n" + command += "cd " + working_dir + "/cfmesh-cfdof \n" + command += "./Allwmake \n" + command += "exit \n" + command += "EOT" + + self.consoleMessage("Starting cfMesh build on " + self.hostname + ".") + self.consoleMessage("This takes a bit of time. Please wait.") + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + " << " + command)[0] + self.consoleMessage("Successfully built and installed cfMesh.") + except: + self.consoleMessage("Could not build cfMesh: " + command_result) + return + + + # old version, not used anymore. Doesn't handle remote install + def downloadInstallCfMesh(self): + print("Error:downloadInstallCFMesh has been depreciated.") + return + + if not self.showAdministratorWarningMessage(): + return + + runtime = self.testGetRuntime(False) + if runtime == "MinGW" and self.form.le_cfmesh_url.text() == CFMESH_URL: + # Openfoam might have just been installed and the URL would not have had a chance to update + self.setDownloadURLs() + + if self.createThread(): + self.thread.task = DOWNLOAD_CFMESH + # We are forced to apply the foam dir selection - reset when the task finishes + CfdTools.setFoamDir(self.foam_dir) + self.thread.cfmesh_url = self.form.le_cfmesh_url.text() + self.thread.start() + + def pickCfMeshFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose cfMesh archive', filter="*.zip") + if f and os.access(f, os.R_OK): + self.form.le_cfmesh_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallHisa(self): + if not self.showAdministratorWarningMessage(): + return + + runtime = self.testGetRuntime(False) + if runtime == "MinGW" and self.form.le_hisa_url.text() == HISA_URL: + # Openfoam might have just been installed and the URL would not have had a chance to update + self.setDownloadURLs() + + if self.createThread(): + self.thread.task = DOWNLOAD_HISA + # We are forced to apply the foam dir selection - reset when the task finishes + CfdTools.setFoamDir(self.foam_dir) + self.thread.hisa_url = self.form.le_hisa_url.text() + self.thread.start() + + def pickHisaFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose HiSA archive', filter="*.zip") + if f and os.access(f, os.R_OK): + self.form.le_hisa_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + + def createThread(self): + if self.thread and self.thread.isRunning(): + self.consoleMessage("Busy - please wait...", 'Error') + return False + else: + self.thread = CfdRemotePreferencePageThread() + self.thread.signals.error.connect(self.threadError) + self.thread.signals.finished.connect(self.threadFinished) + self.thread.signals.status.connect(self.threadStatus) + self.thread.signals.downloadProgress.connect(self.downloadProgress) + return True + + def threadStatus(self, msg): + self.consoleMessage(msg) + + def threadError(self, msg): + self.consoleMessage(msg, 'Error') + self.consoleMessage("Download unsuccessful") + + def threadFinished(self, status): + if self.thread.task == DOWNLOAD_CFMESH: + if status: + if CfdTools.getFoamRuntime() != "MinGW": + self.consoleMessage("Download completed") + user_dir = self.thread.user_dir + self.consoleMessage("Building cfMesh. Lengthy process - please wait...") + self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, CFMESH_FILE_BASE)) + if CfdTools.getFoamRuntime() == 'WindowsDocker': + # There seem to be issues when using multi processors to build in docker + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=1; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=`nproc`; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.consoleMessage("Install completed") + # Reset foam dir for now in case the user presses 'Cancel' + CfdTools.setFoamDir(self.initial_remote_foam_dir) + elif self.thread.task == DOWNLOAD_HISA: + if status: + if CfdTools.getFoamRuntime() != "MinGW": + self.consoleMessage("Download completed") + user_dir = self.thread.user_dir + self.consoleMessage("Building HiSA. Please wait...") + self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, HISA_FILE_BASE)) + if CfdTools.getFoamRuntime() == 'WindowsDocker': + # There seem to be issues when using multi processors to build in docker + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=1; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=`nproc`; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.consoleMessage("Install completed") + # Reset foam dir for now in case the user presses 'Cancel' + CfdTools.setFoamDir(self.initial_remote_foam_dir) + elif self.thread.task == DOWNLOAD_DOCKER: + if status: + self.consoleMessage("Download completed") + else: + self.consoleMessage("Download unsuccessful") + self.thread = None + + def installFinished(self, exit_code): + if exit_code: + self.consoleMessage("Install finished with error {}".format(exit_code)) + else: + self.consoleMessage("Install completed") + + def downloadProgress(self, bytes_done, bytes_total): + mb_done = float(bytes_done)/(1024*1024) + msg = "Downloaded {:.2f} MB".format(mb_done) + if bytes_total > 0: + msg += " of {:.2f} MB".format(float(bytes_total)/(1024*1024)) + self.form.labelDownloadProgress.setText(msg) + + def stderrFilter(self, text): + # Print to stdout rather than stderr so as not to alarm the user + # with the spurious wmkdep errors on stderr + print(text, end='') + return '' + + def dockerCheckboxClicked(self): + if CfdTools.docker_container==None: + CfdTools.docker_container = CfdTools.DockerContainer() + CfdTools.docker_container.usedocker = self.form.cb_docker_sel.isChecked() + self.form.pb_download_install_docker.setEnabled(CfdTools.docker_container.usedocker) + self.form.pb_download_install_openfoam.setEnabled(not CfdTools.docker_container.usedocker) + #self.form.pb_download_install_hisa.setEnabled((not CfdTools.docker_container.usedocker )) + #self.form.pb_download_install_cfMesh.setEnabled((not CfdTools.docker_container.usedocker)) + self.form.gb_docker.setVisible(CfdTools.docker_container.docker_cmd!=None or CfdTools.docker_container.usedocker) + + def downloadInstallDocker(self): + # Set foam dir and output dir in preparation for using docker + CfdTools.setFoamDir(self.form.le_foam_dir.text()) + self.saveSettings() + if self.createThread(): + self.thread.task = DOWNLOAD_DOCKER + self.thread.docker_url = self.form.le_docker_url.text() + self.thread.start() + +class CfdRemotePreferencePageSignals(QObject): + error = QtCore.Signal(str) # Signal in PySide, pyqtSignal in PyQt + finished = QtCore.Signal(bool) + status = QtCore.Signal(str) + downloadProgress = QtCore.Signal(int, int) + + +class CfdRemotePreferencePageThread(QThread): + """ Worker thread to complete tasks in preference page """ + def __init__(self): + super(CfdRemotePreferencePageThread, self).__init__() + self.signals = CfdRemotePreferencePageSignals() + self.quit = False + self.user_dir = None + self.task = None + self.openfoam_url = None + self.paraview_url = None + self.cfmesh_url = None + self.hisa_url = None + self.docker_url = None + + def run(self): + self.quit = False + + try: + if self.task == DOWNLOAD_OPENFOAM: + self.downloadOpenFoam() + elif self.task == DOWNLOAD_PARAVIEW: + self.downloadParaview() + elif self.task == DOWNLOAD_CFMESH: + self.downloadCfMesh() + elif self.task == DOWNLOAD_HISA: + self.downloadHisa() + elif self.task == DOWNLOAD_DOCKER: + self.downloadDocker() + except Exception as e: + if self.quit: + self.signals.finished.emit(False) # Exit quietly since UI already destroyed + return + else: + self.signals.error.emit(str(e)) + self.signals.finished.emit(False) + return + self.signals.finished.emit(True) + + def downloadFile(self, url, **kwargs): + block_size = kwargs.get('block_size', 10*1024) + context = kwargs.get('context', None) + reporthook = kwargs.get('reporthook', None) + suffix = kwargs.get('suffix', '') + with closing(urlrequest.urlopen(url, context=context)) as response: # For Python < 3.3 backward compatibility + download_len = int(response.info().get('Content-Length', 0)) + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp_file: + i = 0 + while True: + data = response.read(block_size) + if not data: + break + if self.quit: + raise RuntimeError("Premature termination received") + tmp_file.write(data) + i += 1 + if reporthook: + reporthook(i, block_size, download_len) + filename = tmp_file.name + return filename, response.info() + + def download(self, url, suffix, name): + self.signals.status.emit("Downloading {}, please wait...".format(name)) + try: + if hasattr(ssl, 'create_default_context'): + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + else: + context = None + # Download + (filename, header) = self.downloadFile( + url, suffix=suffix, reporthook=self.downloadStatus, context=context) + except Exception as ex: + raise Exception("Error downloading {}: {}".format(name, str(ex))) + + self.signals.status.emit("{} downloaded to {}".format(name, filename)) + return filename + + def downloadOpenFoam(self): + filename = self.download(self.openfoam_url, OPENFOAM_FILE_EXT, "OpenFOAM") + if QtCore.QProcess().startDetached(filename): + self.signals.status.emit("OpenFOAM installer launched - please complete the installation") + else: + raise Exception("Failed to launch OpenFOAM installer") + + def downloadParaview(self): + filename = self.download(self.paraview_url, PARAVIEW_FILE_EXT, "ParaView") + if QtCore.QProcess().startDetached(filename): + self.signals.status.emit("ParaView installer launched - please complete the installation") + else: + raise Exception("Failed to launch ParaView installer") + + def downloadCfMesh(self): + filename = self.download(self.cfmesh_url, CFMESH_FILE_EXT, "cfMesh") + + if CfdTools.getFoamRuntime() == "MinGW": + self.user_dir = None + self.signals.status.emit("Installing cfMesh...") + CfdTools.runFoamCommand( + '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. + format(CfdTools.translatePath(filename))) + else: + self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] + # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. + if CfdTools.getFoamRuntime() != 'WindowsDocker': + self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) + + self.signals.status.emit("Extracting cfMesh...") + if CfdTools.getFoamRuntime() == 'WindowsDocker': + from zipfile import ZipFile + with ZipFile(filename, 'r') as zip: + with tempfile.TemporaryDirectory() as tempdir: + zip.extractall(path=tempdir) + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' + .format(CfdTools.translatePath(os.path.join(tempdir, CFMESH_FILE_BASE)))) + else: + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. + format(CFMESH_FILE_BASE, CfdTools.translatePath(filename))) + + def downloadHisa(self): + filename = self.download(self.hisa_url, HISA_FILE_EXT, "HiSA") + + if CfdTools.getFoamRuntime() == "MinGW": + self.user_dir = None + self.signals.status.emit("Installing HiSA...") + CfdTools.runFoamCommand( + '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. + format(CfdTools.translatePath(filename))) + else: + self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] + # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. + if CfdTools.getFoamRuntime() != 'WindowsDocker': + self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) + + self.signals.status.emit("Extracting HiSA...") + if CfdTools.getFoamRuntime() == 'WindowsDocker': + from zipfile import ZipFile + with ZipFile(filename, 'r') as zip: + with tempfile.TemporaryDirectory() as tempdir: + zip.extractall(path=tempdir) + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' + .format(CfdTools.translatePath(os.path.join(tempdir, HISA_FILE_BASE)))) + else: + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. + format(HISA_FILE_BASE, CfdTools.translatePath(filename))) + + def downloadDocker(self): + self.signals.status.emit("Downloading Docker image, please wait until 'Download completed' message shown below") + if CfdTools.docker_container.container_id!=None: + CfdTools.docker_container.stop_container() + cmd = '{} pull {}'.format(CfdTools.docker_container.docker_cmd, self.docker_url) + if 'docker'in CfdTools.docker_container.docker_cmd: + cmd = cmd.replace('docker.io/','') + + CfdTools.runFoamCommand(cmd) + + def downloadStatus(self, blocks, block_size, total_size): + self.signals.downloadProgress.emit(blocks*block_size, total_size) diff --git a/CfdOF/CfdTools.py b/CfdOF/CfdTools.py index da566e89..2507b073 100644 --- a/CfdOF/CfdTools.py +++ b/CfdOF/CfdTools.py @@ -46,6 +46,11 @@ from CfdOF.CfdConsoleProcess import CfdConsoleProcess from CfdOF.CfdConsoleProcess import removeAppimageEnvironment from PySide import QtCore + +# added for runCommand +from PySide.QtCore import QProcess # I added +from PySide.QtGui import QApplication # I added + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtGui @@ -91,6 +96,20 @@ def getDefaultOutputPath(): output_path = os.path.normpath(output_path) return output_path +# TODO: Is this used anymore ? +def getDefaultRemoteOutputPath(): + prefs = getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultRemoteOutputPath", "") + if not output_path: + #TODO: fix this so it will run on the server + #should be /home/username from the reference page + # output_path = tempfile.gettempdir() + output_path = "" + # assume the remote computer is Linux and + # don't adjust the output path + # output_path = os.path.normpath(output_path) + return output_path + def getOutputPath(analysis): if analysis and 'OutputPath' in analysis.PropertiesList: @@ -102,6 +121,19 @@ def getOutputPath(analysis): output_path = os.path.normpath(output_path) return output_path +# TODO Is this used anymore ? +def getRemoteOutputPath(analysis): + if analysis and 'RemoteOutputPath' in analysis.PropertiesList: + output_path = analysis.RemoteOutputPath + else: + output_path = "" + if not output_path: + output_path = getDefaultRemoteOutputPath() + # Assume the remote computer is Linux and don't + # adjust the path + # output_path = os.path.normpath(output_path) + return output_path + # Get functions if FreeCAD.GuiUp: @@ -470,12 +502,25 @@ def getPreferencesLocation(): # Set parameter location return "User parameter:BaseApp/Preferences/Mod/CfdOF" - def setFoamDir(installation_path): prefs = getPreferencesLocation() # Set OpenFOAM install path in parameters FreeCAD.ParamGet(prefs).SetString("InstallationPath", installation_path) +#not used anymore +def setRemoteFoamDir(remote_installation_path): + print("Error: setRemoteFoamDir is depreciated.") + prefs = getPreferencesLocation() + # Set OpenFOAM remote install path in parameters + FreeCAD.ParamGet(prefs).SetString("RemoteInstallationPath", remote_installation_path) + +# not used anymore +def getRemoteFoamDir(): + print("Error: getRemoteFoamDir is depreciated.") + prefs = getPreferencesLocation() + # Set OpenFOAM remote install path in parameters + return FreeCAD.ParamGet(prefs).GetString("RemoteInstallationPath", "") + def startDocker(): global docker_container if docker_container==None: @@ -638,7 +683,6 @@ def setParaviewPath(paraview_path): # Set Paraview install path in parameters FreeCAD.ParamGet(prefs).SetString("ParaviewPath", paraview_path) - def getParaviewPath(): prefs = getPreferencesLocation() # Get path from parameters @@ -653,6 +697,12 @@ def setGmshPath(gmsh_path): # Set Paraview install path in parameters FreeCAD.ParamGet(prefs).SetString("GmshPath", gmsh_path) +# not used anymore +def setRemoteGmshPath(gmsh_path): + print("Error: setRemoteGmshPath is depreciated.") + prefs = getPreferencesLocation() + # Set Paraview install path in parameters + FreeCAD.ParamGet(prefs).SetString("RemoteGmshPath", gmsh_path) def getGmshPath(): prefs = getPreferencesLocation() @@ -662,6 +712,16 @@ def getGmshPath(): setGmshPath(gmsh_path) return gmsh_path +# not used anymore +def getRemoteGmshPath(): + print("Error: getRemoteGmshPath is depreciated.") + prefs = getPreferencesLocation() + # Get path from parameters + gmsh_path = FreeCAD.ParamGet(prefs).GetString("RemoteGmshPath", "") + # Ensure parameters exist for future editing + setGmshPath(gmsh_path) + return gmsh_path + def translatePath(p): """ @@ -794,7 +854,13 @@ def getRunEnvironment(): else: return {} - +# This routine is used by both the remote meshing process and the remote solver process +# to convert the remote command into a command that will run with CfDConsole process +# However, it adds a bash call that it doesn't need to and it sources the OpenFOAM init file +# on the local computer, which it doesn't have to. When a remote host is used, it is assumed +# that OpenFOAM will run from the command line. This means its init file is run in bashrc file. +# This is probably a dangerous assumption and this routine should be changed to work with remote +# hosts and source the OpenFOAM init file. def makeRunCommand(cmd, dir, source_env=True): """ Generate native command to run the specified Linux command in the relevant environment, @@ -928,6 +994,7 @@ def runFoamCommand(cmdline, case=None): return proc.output, proc.outputErr, proc.outputAll + def startFoamApplication(cmd, case, log_name='', finished_hook=None, stdout_hook=None, stderr_hook=None): """ Run command cmd in OpenFOAM environment, sending output to log file. @@ -1274,6 +1341,71 @@ def checkCfdDependencies(): print("Completed CFD dependency check") return message +#****************************************************************************** +# Done: TODO: right now this routine returns the error code of the outermost process, ie the one called with cmd parameter. +# So if you call ssh, for example, and it runs successfully, it will return no error, no matter what happens to the command +# that was used in ssh. This should be changed. If one gets an error message via stderr, this routine should +# return an error code, regardless of how ssh itself ran. +# +# TODO: add a timer so that the process times out if it doesn't create any output or do anything. +# If ssh credentials aren't set up properly on the remote host, it will ask for a password and not echo that to stdout or stderr. +# Thus the process will be stuck. +# +# This routine is not needed. runFoamCommand does the same thing but is integrated into CfdTools already. +# The only routines that use runCommand are ping and ssh test in RemotePreferences.py. Those routines should be +# rewritten with runFoamCommand and put in CfdTools. +# +# When that happens, this routine can be deleted. + +def runCommand(cmd, args=[]): + + process = QProcess() + returnValue = 0 + + def handleStdout(): + #print("In Stdout handler") + data = process.readAllStandardOutput() + message = bytes(data).decode("utf8") + print(message) + + def handleStderr(): + nonlocal returnValue + #print("In Stderr handler") + data = process.readAllStandardError() + message = bytes(data).decode("utf8") + print("Error:" + message) + returnValue = -1 + + def handleFinished(self, exitCode): + nonlocal returnValue + print("Command done.") + if ((exitCode == QProcess.NormalExit) and (returnValue == 0)): + returnValue = 0 + else: + returnValue = -1 + + # connect the events to handlers + process.readyReadStandardOutput.connect(handleStdout) + process.readyReadStandardError.connect(handleStderr) + process.finished.connect(handleFinished) + + # debug + message = "Remotely executing: " + cmd + for arg in args: + message = message + " " + arg + print(message) + + # start the command process + process.start(cmd, args) + + # wait for the process to get started + process.waitForStarted() + + # process application events while waiting for it to finish + while(process.state() == QProcess.Running): + QApplication.processEvents() + return(returnValue) + def getParaviewExecutable(): # If path of paraview executable specified, use that @@ -1308,6 +1440,22 @@ def getGmshExecutable(): gmsh_cmd='gmsh' return gmsh_cmd +def getRemoteGmshExecutable(): + # If path of gmsh executable specified, use that + gmsh_cmd = getGmshPath() + if not gmsh_cmd: + # On Windows, use gmsh supplied + if platform.system() == "Windows": + # Use forward slashes to avoid escaping problems + gmsh_cmd = '/'.join([FreeCAD.getHomePath().rstrip('/'), 'bin', 'gmsh.exe']) + if not gmsh_cmd: + # Otherwise, see if the command 'gmsh' is in the path. + gmsh_cmd = shutil.which("gmsh") + if getFoamRuntime() == "PosixDocker": + gmsh_cmd='gmsh' + return gmsh_cmd + + def startParaview(case_path, script_name, console_message_fn): proc = QtCore.QProcess() @@ -1774,6 +1922,11 @@ def run(self, cmdline, case=None): if startDocker(): return 1 print("Running ", cmdline) + + mycmdline = makeRunCommand(cmdline,case) + print("cmdline in CfdSynFoamProcess:") + print(mycmdline) + self.process.start(makeRunCommand(cmdline, case), env_vars=getRunEnvironment()) if not self.process.waitForFinished(): raise Exception("Unable to run command " + cmdline) @@ -1787,6 +1940,8 @@ def readError(self, output): self.outputErr += output self.outputAll += output + + # Only one container is needed. Start one for the CfdOF workbench as required class DockerContainer: container_id = None diff --git a/CfdOF/Mesh/CfdMesh.py b/CfdOF/Mesh/CfdMesh.py index 6401412e..0ac2937a 100644 --- a/CfdOF/Mesh/CfdMesh.py +++ b/CfdOF/Mesh/CfdMesh.py @@ -102,12 +102,24 @@ def initProperties(self, obj): "Mesh elements per 360 degrees for surface triangulation with GMSH") addObjectProperty(obj, 'NumberOfProcesses', 1, "App::PropertyInteger", "", - "Number of parallel processes (only applicable to cfMesh and snappyHexMesh)") + "Number of parallel processes on local computer (only applicable to cfMesh and snappyHexMesh)") addObjectProperty(obj, 'NumberOfThreads', 0, "App::PropertyInteger", "", - "Number of parallel threads per process (only applicable to cfMesh and gmsh). " + "Number of parallel threads per process on local computer (only applicable to cfMesh and gmsh). " "0 means use all available (if NumberOfProcesses = 1) or use 1 (if NumberOfProcesses > 1)") + + # I added the remote processes and threads + """ + addObjectProperty(obj, 'NumberOfRemoteProcesses', 1, "App::PropertyInteger", "", + "Number of parallel processes on remote computer (only applicable to cfMesh and snappyHexMesh)") + + addObjectProperty(obj, 'NumberOfRemoteThreadsPerProcess', 0, "App::PropertyInteger", "", + "Number of parallel threads per process on remote computer (only applicable to cfMesh and gmsh). " + "0 means use all available (if NumberOfProcesses = 1) or use 1 (if NumberOfProcesses > 1)") + """ + + addObjectProperty(obj, "Part", None, "App::PropertyLinkGlobal", "Mesh Parameters", "Part object to mesh") if addObjectProperty(obj, "MeshUtility", MESHERS, "App::PropertyEnumeration", diff --git a/CfdOF/Mesh/CfdMeshTools.py b/CfdOF/Mesh/CfdMeshTools.py index c73fb60f..5bd39ac0 100644 --- a/CfdOF/Mesh/CfdMeshTools.py +++ b/CfdOF/Mesh/CfdMeshTools.py @@ -70,7 +70,7 @@ def __init__(self, cart_mesh_obj): self.progressCallback = None - def writeMesh(self): + def writeMesh(self, profile_name): self.setupMeshCaseDir() CfdTools.cfdMessage("Exporting mesh refinement data ...\n") if self.progressCallback: @@ -81,10 +81,8 @@ def writeMesh(self): if self.progressCallback: self.progressCallback("Exporting the part surfaces ...") self.writePartFile() - self.writeMeshCase() - CfdTools.cfdMessage("Wrote mesh case to {}\n".format(self.meshCaseDir)) - if self.progressCallback: - self.progressCallback("Mesh case written successfully") + self.writeMeshCase(profile_name) + def processExtrusions(self): """ Find and process any extrusion objects """ @@ -584,7 +582,7 @@ def loadSurfMesh(self): else: print('No mesh was created.') - def writeMeshCase(self): + def writeMeshCase(self, host_profile): """ Collect case settings, and finally build a runnable case. """ CfdTools.cfdMessage("Populating mesh dictionaries in folder {}\n".format(self.meshCaseDir)) @@ -709,14 +707,20 @@ def writeMeshCase(self): if CfdTools.getFoamRuntime() != 'WindowsDocker': self.settings['TranslatedFoamPath'] = CfdTools.translatePath(CfdTools.getFoamDir()) + # set the number of threads and processes + # these were set appropriately when the host was selected if self.mesh_obj.NumberOfProcesses <= 1: - self.settings['ParallelMesh'] = False - self.settings['NumberOfProcesses'] = 1 + self.settings['ParallelMesh'] = False + self.settings['NumberOfProcesses'] = 1 else: - self.settings['ParallelMesh'] = True - self.settings['NumberOfProcesses'] = self.mesh_obj.NumberOfProcesses + self.settings['ParallelMesh'] = True + self.settings['NumberOfProcesses'] = self.mesh_obj.NumberOfProcesses + self.settings['NumberOfThreads'] = self.mesh_obj.NumberOfThreads + if self.progressCallback: + self.progressCallback("Mesh case will use " + str(self.settings['NumberOfProcesses']) + " processes and " + str(self.settings['NumberOfThreads']) + " threads per process.") + self.progressCallback("0 threads per process means use all available (if NumberOfProcesses = 1) or use 1 per process (if NumberOfProcesses > 1)") TemplateBuilder(self.meshCaseDir, self.template_path, self.settings) # Update Allmesh permission - will fail silently on Windows @@ -726,7 +730,34 @@ def writeMeshCase(self): os.chmod(fname, s.st_mode | stat.S_IEXEC) self.analysis.NeedsMeshRewrite = False - CfdTools.cfdMessage("Successfully wrote meshCase to folder {}\n".format(self.meshCaseDir)) + CfdTools.cfdMessage("Successfully wrote meshCase to local folder {}\n".format(self.meshCaseDir)) + if self.progressCallback: + self.progressCallback("Successfully wrote meshCase to local folder {}\n".format(self.meshCaseDir)) + + #if this is a remote mesh, copy the mesh case folder from the local mesh case dir + # to the remote host's directory + if host_profile != "local": + profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + host_profile + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + + # rsync the meshCase directory to the remote host's output directory + # Typical useage: rsync -r --delete /tmp/meshCase me@david:/tmp + try: + CfdTools.runFoamCommand("rsync -r --delete " + self.meshCaseDir + " " + remote_user + "@" + remote_hostname + \ + ":" + remote_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy meshCase to remote host: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not copy meshCase to remote host: " + str(e)) + else: + CfdTools.cfdMessage("Successfully copied local meshCase to folder " + remote_output_path + " on remote host " + remote_hostname + "\n" ) + if self.progressCallback: + self.progressCallback("Successfully copied local meshCase to folder " + remote_output_path + " on remote host " + remote_hostname + "\n") + if self.progressCallback: + self.progressCallback("Mesh case write process is complete.") + def writeSurfaceMeshFromShape(shape, path, name, mesh_obj): prefs = CfdTools.getPreferencesLocation() diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 9941fb60..75afc583 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -21,6 +21,29 @@ # * see . * # * * # *************************************************************************** +# +# LinuxGuy123@gmail.com's notes: +# +# +# TODOs, in addition to TODOs in the code itself +# +# - This code uses (dangerous) global vars to access things like profile_name, hostname, output dir, etc. +# The profile_name, hostname, output dir should be attached to the mesh object and used from it. +# +#- Add use filename extension to the output path. For both local and remote processing. +# The value is already saved in prefs. You can get it with FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) +# Should be appended to the mesh object ? +# +# - right now there is no way to edit the case on a remote host. This could be enabled by +# copying back the case to the local machine, allowing the user to edit the files in a temp dir +# and then copying them back to the remote host +# +#- copy the mesh back from the host to the local computer for Paraview, Load surface mesh and Check Mesh. +# +#- enable check mesh for remote hosts +#- enable ParaView for remote hosts +#- enable Load Surface mesh for remote hosts +#- enable Clear Surface mesh for remote hosts from __future__ import print_function import FreeCAD @@ -58,14 +81,55 @@ def __init__(self, obj): stdout_hook=self.gotOutputLines, stderr_hook=self.gotErrorLines) + #set the prefs and host prefs locations + self.prefs_location = CfdTools.getPreferencesLocation() + self.host_prefs_location = self.prefs_location + "/Hosts" + self.useRemoteProcessing = FreeCAD.ParamGet(self.prefs_location).GetBool('UseRemoteProcessing', 0) + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + + self.profile_name = "" + self.hostname = "" + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.gmsh_path = "" + self.add_filename_to_output = False + + #add a local host to cb_profile + self.form.cb_profile.addItem("local") + + # if using remote processing, add the host profiles as well + if self.useRemoteProcessing: + self.loadProfileNames() + else: + #disable cb_profile so that users aren't trying to change the host + self.form.cb_profile.setEnabled(False) + + # load the local profile + self.loadProfile("local") + self.Timer = QtCore.QTimer() self.Timer.setInterval(1000) self.Timer.timeout.connect(self.update_timer_text) + # set up the profiles combo box connection + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) + self.form.cb_utility.activated.connect(self.choose_utility) + self.form.pb_write_mesh.clicked.connect(self.writeMesh) + self.form.pb_edit_mesh.clicked.connect(self.editMesh) + self.form.pb_run_mesh.clicked.connect(self.runMesh) + self.form.pb_stop_mesh.clicked.connect(self.killMeshProcess) self.form.pb_paraview.clicked.connect(self.openParaview) self.form.pb_load_mesh.clicked.connect(self.pbLoadMeshClicked) @@ -97,6 +161,135 @@ def __init__(self, obj): self.Start = time.time() self.Timer.start() + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + print("Error: no host profile selected") + return + + # set the vars to the local parameters + if profile_name == "local": + self.hostname = "local" + # enable controls for the local host + self.form.pb_edit_mesh.setEnabled(True) + self.form.pb_paraview.setEnabled(True) + self.form.pb_check_mesh.setEnabled(True) + self.form.pb_load_mesh.setEnabled(True) + self.form.pb_clear_mesh.setEnabled(True) + # the local code doesn't use these vars, so don't set them + # dangerous. + """ + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = "" + self.add_filename_to_output = False + """ + else: + # set the vars to the remote host parameters + # most of these aren't used, at least not in this page + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + + #now set the control values + self.mesh_obj.NumberOfProcesses = self.mesh_processes + self.mesh_obj.NumberOfThreads = self.mesh_threads + + # disable if using a remote host + self.form.pb_edit_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + self.form.pb_clear_mesh.setEnabled(False) + + #TODO: fix these, if we need to. + #self.form.le_mesh_processes.setText(str(self.mesh_processes)) + #self.form.le_mesh_threads.setText(str(self.mesh_threads)) + + #self.form.le_hostname.setText(self.hostname) + #self.form.le_username.setText(self.username) + + #self.form.le_foam_processes.setText(str(self.foam_processes)) + #self.form.le_foam_threads.setText(str(self.foam_threads)) + #self.form.le_foam_dir.setText(self.foam_dir) + #self.form.le_output_path.setText(self.output_path) + #self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + + + # this gets called when the user changes the profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + + # TODO enable and disable the appropriate controls here + # Remote hosts can't edit the case nor Paraview, check mesh, etc. + # Nor load surface mesh nor clear surface mesh. + #updateUI + + # test routine to run a mesh without a proxy + # The real routine is runMesh way below + def runRemoteMesh(self): + # run remote meshing directly, without a proxy + #profile_prefs = CfdTools.getPreferencesLocation() + '/Hosts/' + self.profile_name + remote_user = self.username + remote_hostname = self.hostname + + # create the ssh connection command + ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' ' + + # Get the working directory for the mesh + working_dir = self.output_path + #TODO: add filename to the path if selected + + # create the command to do the actual work + command = 'EOT \n' + command += 'cd ' + working_dir + '/meshCase \n' + command += './Allmesh \n' + command += 'exit \n' + command += 'EOT' + command = ssh_prefix + ' << ' + command + + self.consoleMessage("Starting remote meshing...") + try: + CfdTools.runFoamCommand(command) + print("Remote meshing is complete.") + self.consoleMessage("Remote meshing is complete.") + except Exception as error: + self.consoleMessage("Error meshing on remote host: " + str(error)) + def getStandardButtons(self): return int(QtGui.QDialogButtonBox.Close) # def reject() is called on close button @@ -131,20 +324,34 @@ def load(self): (self.mesh_obj.MeshUtility, self.mesh_obj.ElementDimension, self.mesh_obj.ConvertToDualMesh), 0) self.form.cb_utility.setCurrentIndex(index_utility) + #TODO: this should be changed to reflect if the host is local or remote def updateUI(self): + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir - self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) - self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) - self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) - self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) - self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) - utility = CfdMesh.MESHERS[self.form.cb_utility.currentIndex()] if utility == "snappyHexMesh": self.form.snappySpecificProperties.setVisible(True) else: self.form.snappySpecificProperties.setVisible(False) + # enable the appropriate controls + self.form.pb_write_mesh.setEnabled(True) + if self.profile_name == 'local': + self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) + self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) + self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + + # remote host is being used + else: + # remote hosts don't support these functions yet + self.form.pb_run_mesh.setEnabled(True) + self.form.pb_paraview.setEnabled(False) + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + self.form.pb_clear_mesh.setEnabled(False) + def store(self): mesher_idx = self.form.cb_utility.currentIndex() storeIfChanged(self.mesh_obj, 'CharacteristicLengthMax', getQuantity(self.form.if_max)) @@ -199,6 +406,10 @@ def writeMesh(self): # Re-initialise CfdMeshTools with new parameters self.store() + #get the host name we are writing the mesh case for + host_profile = self.profile_name + print ("Writing mesh for host profile " + host_profile + ".") + FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") FreeCADGui.doCommand("from CfdOF import CfdTools") FreeCADGui.doCommand("cart_mesh = " @@ -208,7 +419,10 @@ def writeMesh(self): cart_mesh.progressCallback = self.progressCallback # Start writing the mesh files - self.consoleMessage("Preparing meshing ...") + if host_profile == "local": + self.consoleMessage("Preparing local mesh ...") + else: + self.consoleMessage("Preparing remote mesh for " + host_profile + "...") try: QApplication.setOverrideCursor(Qt.WaitCursor) setQuantity(self.form.if_max, str(cart_mesh.getClmax())) @@ -219,7 +433,12 @@ def writeMesh(self): + cart_mesh.part_obj.Label + ', ShapeType: ' + cart_mesh.part_obj.Shape.ShapeType) print(' CharacteristicLengthMax: ' + str(cart_mesh.clmax)) - FreeCADGui.doCommand("cart_mesh.writeMesh()") + + if host_profile == "local": + FreeCADGui.doCommand("cart_mesh.writeMesh('local')") + else: + FreeCADGui.doCommand("cart_mesh.writeMesh('"+ host_profile +"')") + except Exception as ex: self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') raise @@ -231,6 +450,7 @@ def writeMesh(self): # Update the UI self.updateUI() + def progressCallback(self, message): self.consoleMessage(message) @@ -262,6 +482,7 @@ def checkMeshClicked(self): self.form.pb_check_mesh.setEnabled(False) # Prevent user running a second instance self.form.pb_run_mesh.setEnabled(False) self.form.pb_write_mesh.setEnabled(False) + #self.form.pb_write_remote_mesh.setEnabled(False) self.form.pb_stop_mesh.setEnabled(False) self.form.pb_paraview.setEnabled(False) self.form.pb_load_mesh.setEnabled(False) @@ -275,12 +496,14 @@ def checkMeshClicked(self): finally: QApplication.restoreOverrideCursor() + def editMesh(self): case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir self.consoleMessage("Please edit the case input files externally at: {}\n".format(case_path)) CfdTools.openFileManager(case_path) def runMesh(self): + # TODO: this only works for local processing. Not remote processing. if CfdTools.getFoamRuntime() == "PosixDocker": CfdTools.startDocker() @@ -303,32 +526,95 @@ def runMesh(self): try: QApplication.setOverrideCursor(Qt.WaitCursor) + self.consoleMessage("Initializing {} ...".format(self.mesh_obj.MeshUtility)) FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") FreeCADGui.doCommand("from CfdOF import CfdTools") FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") - FreeCADGui.doCommand("cart_mesh = " + FreeCADGui.doCommand("from FreeCAD import ParamGet") + + FreeCADGui.doCommand("cart_mesh = " + "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") + FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") FreeCADGui.doCommand("cart_mesh.error = False") - FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False)") - FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") - FreeCADGui.doCommand("proxy.running_from_macro = True") - self.mesh_obj.Proxy.running_from_macro = False - FreeCADGui.doCommand("if proxy.running_from_macro:\n" + - " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + - " mesh_process.start(cmd, env_vars=env_vars)\n" + - " mesh_process.waitForFinished()\n" + - "else:\n" + - " proxy.mesh_process.start(cmd, env_vars=env_vars)") + + # run locally + if self.profile_name == "local": + FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False)") + FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") + + FreeCADGui.doCommand("print('cmd:')") + FreeCADGui.doCommand("print(cmd)") + #FreeCADGui.doCommand("print('env_vars:' + env_vars)") + + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(cmd, env_vars=env_vars)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(cmd, env_vars=env_vars)") + + # run on remote host + else: + #self.runRemoteMesh() #For testing the non proxy function above + + # Get the username and hostname for the remote host + prefsCmd = "profile_prefs = CfdTools.getPreferencesLocation() + " + '"/Hosts/' + self.profile_name + '"' + #print("prefsCmd:" + prefsCmd) + FreeCADGui.doCommand(prefsCmd) + FreeCADGui.doCommand("print('profile_prefs:' + profile_prefs)") + + FreeCADGui.doCommand("remote_user = FreeCAD.ParamGet(profile_prefs).GetString('Username', '')") + FreeCADGui.doCommand("remote_hostname =FreeCAD.ParamGet(profile_prefs).GetString('Hostname', '')") + + #FreeCADGui.doCommand("print('username:' + remote_user)") + #FreeCADGui.doCommand("print('hostname:' + remote_hostname)") + + # create the ssh connection command + FreeCADGui.doCommand("ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' '") + + # Get the working directory for the mesh + FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") + #FreeCADGui.doCommand("print('working directory:' + working_dir)") + + + # create the command to do the actual work + FreeCADGui.doCommand("command = 'EOT \\n' \n" + + "command += 'cd ' + working_dir + '/meshCase \\n' \n" + + "command += './Allmesh \\n' \n" + + "command += 'exit \\n' \n" + + "command += 'EOT' \n") + FreeCADGui.doCommand("command = ssh_prefix + ' << ' + command + '\\n'") + + #FreeCADGui.doCommand("print(command)") + + FreeCADGui.doCommand("runCommand = CfdTools.makeRunCommand(command, None)") + + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(runCommand)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(runCommand)") + if self.mesh_obj.Proxy.mesh_process.waitForStarted(): - self.form.pb_run_mesh.setEnabled(False) # Prevent user running a second instance + # enable/disable the correct buttons self.form.pb_stop_mesh.setEnabled(True) + self.form.pb_run_mesh.setEnabled(False) self.form.pb_write_mesh.setEnabled(False) + self.form.pb_edit_mesh.setEnabled(False) self.form.pb_check_mesh.setEnabled(False) self.form.pb_paraview.setEnabled(False) self.form.pb_load_mesh.setEnabled(False) + self.form.pb_clear_mesh.setEnabled(False) self.consoleMessage("Mesher started ...") else: self.consoleMessage("Error starting meshing process", 'Error') @@ -339,6 +625,7 @@ def runMesh(self): finally: QApplication.restoreOverrideCursor() + def killMeshProcess(self): self.consoleMessage("Meshing manually stopped") self.error_message = 'Meshing interrupted' @@ -357,20 +644,10 @@ def meshFinished(self, exit_code): if exit_code == 0: self.consoleMessage('Meshing completed') self.analysis_obj.NeedsMeshRerun = False - self.form.pb_run_mesh.setEnabled(True) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(True) - self.form.pb_write_mesh.setEnabled(True) - self.form.pb_check_mesh.setEnabled(True) - self.form.pb_load_mesh.setEnabled(True) else: self.consoleMessage("Meshing exited with error", 'Error') - self.form.pb_run_mesh.setEnabled(True) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(True) - self.form.pb_check_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) + # update the controls self.error_message = '' # Get rid of any existing loaded mesh self.pbClearMeshClicked() diff --git a/CfdOF/Solve/CfdCaseWriterFoam.py b/CfdOF/Solve/CfdCaseWriterFoam.py index 3626812c..e049bfb7 100644 --- a/CfdOF/Solve/CfdCaseWriterFoam.py +++ b/CfdOF/Solve/CfdCaseWriterFoam.py @@ -24,7 +24,8 @@ import os import os.path -from FreeCAD import Units +#import FreeCAD +from FreeCAD import Units, ParamGet from CfdOF import CfdTools from CfdOF.TemplateBuilder import TemplateBuilder from CfdOF.CfdTools import cfdMessage @@ -56,7 +57,7 @@ def __init__(self, analysis_obj): self.settings = None - def writeCase(self): + def writeCase(self, profile_name): """ writeCase() will collect case settings, and finally build a runnable case. """ cfdMessage("Writing case to folder {}\n".format(self.working_dir)) if not os.path.exists(self.working_dir): @@ -153,8 +154,37 @@ def writeCase(self): cfdMessage("Successfully wrote case to folder {}\n".format(self.working_dir)) if self.progressCallback: - self.progressCallback("Case written successfully") - + self.progressCallback("Case written locally successfully") + + # if using a remote host, copy the case folder from the local case dir + # to the remote host's directory + if profile_name != "local": + profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + profile_name + remote_user = ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = ParamGet(profile_prefs).GetString("OutputPath","") + + #print("remote_user:" + remote_user) + #print("remote_hostname:" + remote_hostname) + #print("remote_output_path:" + remote_output_path) + #print("self.case_folder:" + self.case_folder) + #print("self.working_dir:" + self.working_dir) + + # rsync the meshCase directory to the remote host's output directory + # Typical useage: rsync -r --delete /tmp/ me@david:/tmp + try: + CfdTools.runFoamCommand("rsync -r --delete " + self.case_folder + " " + remote_user + "@" + remote_hostname + \ + ":" + remote_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy case to remote host: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not copy case to remote host: " + str(e)) + return False + else: + CfdTools.cfdMessage("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n" ) + if self.progressCallback: + self.progressCallback("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n") + return True def getSolverName(self): diff --git a/CfdOF/Solve/TaskPanelCfdSolverControl.py b/CfdOF/Solve/TaskPanelCfdSolverControl.py index ef0ae544..364131ea 100644 --- a/CfdOF/Solve/TaskPanelCfdSolverControl.py +++ b/CfdOF/Solve/TaskPanelCfdSolverControl.py @@ -35,6 +35,23 @@ from PySide.QtCore import Qt from PySide.QtGui import QApplication +# LinuxGuy123's Notes +# +# TODOs (there are some in the code as well) +# +#- add filename to the output path. (addFilenameToOutput) For both local and remote useRemoteProcessing. +# It is already saved in prefs, for both local and remote hosts. You can get it with +# FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) +# +#- copy the mesh back to the local computer for Edit and Paraview buttons +# +#- makeRunCommand is using the local OF install dir to build the remote run command. It works but it isn't correct. +# The remote run is not calling the source command to set up OF usage. It is relying on the shell to do that, through +# bashrc and OF working directly from the command line. +# +#- enable and disable buttons appropriately when running and when done +# +# global vars are used for stuff that should be passed via the solver object. class TaskPanelCfdSolverControl: def __init__(self, solver_runner_obj): @@ -61,9 +78,44 @@ def __init__(self, solver_runner_obj): self.working_dir = CfdTools.getOutputPath(self.analysis_object) + #set the prefs and host prefs locations + self.prefs_location = CfdTools.getPreferencesLocation() + self.host_prefs_location = self.prefs_location + "/Hosts" + self.useRemoteProcessing = FreeCAD.ParamGet(self.prefs_location).GetBool('UseRemoteProcessing', 0) + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + self.profile_name = "" + self.hostname = "" + self.username = "" + #self.mesh_processes = 0 + #self.mesh_threads = 0 + #self.foam_processes = 0 + #self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + #self.gmsh_path = "" + self.add_filename_to_output = False + + #add a local host to cb_profile + self.form.cb_profile.addItem("local") + + # if using remote processing, add the host profiles as well + if self.useRemoteProcessing: + self.loadProfileNames() + else: + #disable cb_profile so that users aren't trying to change the host + self.form.cb_profile.setEnabled(False) + + # load the local profile + self.loadProfile("local") + self.updateUI() # Connect Signals and Slots + # set up the profiles combo box connection + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) self.form.pb_write_inp.clicked.connect(self.write_input_file_handler) self.form.pb_edit_inp.clicked.connect(self.editSolverInputFile) self.form.pb_run_solver.clicked.connect(self.runSolverProcess) @@ -72,6 +124,92 @@ def __init__(self, solver_runner_obj): self.Start = time.time() self.Timer.start() + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + print("Error: no host profile selected") + return + + # set the vars to the local parameters + if profile_name == "local": + self.hostname = "local" + # the local code doesn't use these vars, so don't set them + # dangerous. + """ + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = "" + self.add_filename_to_output = False + """ + else: + #set the vars to the remote host parameters + # most of these aren't used, at least not in this page + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + #self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + #self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + #self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + #self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + + #now set the control values + #self.mesh_obj.NumberOfProcesses = self.mesh_processes + #self.mesh_obj.NumberOfThreads = self.mesh_threads + + #TODO: fix these, if we need to. + #self.form.le_mesh_processes.setText(str(self.mesh_processes)) + #self.form.le_mesh_threads.setText(str(self.mesh_threads)) + + #self.form.le_hostname.setText(self.hostname) + #self.form.le_username.setText(self.username) + + #self.form.le_foam_processes.setText(str(self.foam_processes)) + #self.form.le_foam_threads.setText(str(self.foam_threads)) + #self.form.le_foam_dir.setText(self.foam_dir) + #self.form.le_output_path.setText(self.output_path) + #self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + + + # this gets called when the user changes the profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + # TODO enable and disable the appropriate controls here + # Remote hosts can't edit the case nor Paraview, check mesh, etc. + # Nor load surface mesh nor clear surface mesh. + if self.profile_name == 'local': + self.form.pb_write_inp.enabled = True + else: + pass + def updateUI(self): solverDirectory = os.path.join(self.working_dir, self.solver_object.InputCaseName) self.form.pb_edit_inp.setEnabled(os.path.exists(solverDirectory)) @@ -114,31 +252,35 @@ def write_input_file_handler(self): self.consoleMessage("Case writer called") self.form.pb_paraview.setEnabled(False) self.form.pb_edit_inp.setEnabled(False) - self.form.pb_run_solver.setEnabled(False) + self.form.pb_run_solver.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) try: - FreeCADGui.doCommand("FreeCAD.ActiveDocument." + self.solver_object.Name + ".Proxy.case_writer = " + FreeCADGui.doCommand("FreeCAD.ActiveDocument." + self.solver_object.Name + ".Proxy.case_writer = " "CfdCaseWriterFoam.CfdCaseWriterFoam(FreeCAD.ActiveDocument." + self.solver_runner.analysis.Name + ")") - FreeCADGui.doCommand("writer = FreeCAD.ActiveDocument." + + FreeCADGui.doCommand("writer = FreeCAD.ActiveDocument." + self.solver_object.Name + ".Proxy.case_writer") - writer = self.solver_object.Proxy.case_writer - writer.progressCallback = self.consoleMessage - FreeCADGui.doCommand("writer.writeCase()") + writer = self.solver_object.Proxy.case_writer + writer.progressCallback = self.consoleMessage + FreeCADGui.doCommand("writer.writeCase('" + self.profile_name +"')") + except Exception as e: - self.consoleMessage("Error writing case:", 'Error') - self.consoleMessage(type(e).__name__ + ": " + str(e), 'Error') - self.consoleMessage("Write case setup file failed", 'Error') - raise + self.consoleMessage("Error writing case:", 'Error') + self.consoleMessage(type(e).__name__ + ": " + str(e), 'Error') + self.consoleMessage("Write case setup file failed", 'Error') + raise else: - self.analysis_object.NeedsCaseRewrite = False + self.analysis_object.NeedsCaseRewrite = False finally: - QApplication.restoreOverrideCursor() + QApplication.restoreOverrideCursor() self.updateUI() self.form.pb_run_solver.setEnabled(True) + else: self.consoleMessage("Case check failed", 'Error') + + def check_prerequisites_helper(self): self.consoleMessage("Checking dependencies...") @@ -153,7 +295,7 @@ def editSolverInputFile(self): self.consoleMessage("Please edit the case input files externally at: {}\n".format(case_path)) CfdTools.openFileManager(case_path) - def runSolverProcess(self): + def runSolverProcess(self, profileName): self.Start = time.time() # Check for changes that require remesh @@ -180,7 +322,7 @@ def runSolverProcess(self): if self.analysis_object.NeedsMeshRewrite or self.analysis_object.NeedsMeshRerun: from CfdOF.Mesh import CfdMeshTools - mesh_obj = CfdTools.getMeshObject(self.analysis_object) + mesh_obj = CfdTools.getMeshObject(self.analysis_object) #TODO: This won't work with remote hosts. Need to pass the host name into the mesh object cart_mesh = CfdMeshTools.CfdMeshTools(mesh_obj) cart_mesh.progressCallback = self.consoleMessage if self.analysis_object.NeedsMeshRewrite: @@ -202,15 +344,45 @@ def runSolverProcess(self): cart_mesh.error = False cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False) env_vars = CfdTools.getRunEnvironment() - self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) - if self.solver_object.Proxy.solver_process.waitForStarted(): - # Setting solve button to inactive to ensure that two instances of the same simulation aren't started - # simultaneously - self.form.pb_write_inp.setEnabled(False) - self.form.pb_run_solver.setEnabled(False) - self.form.terminateSolver.setEnabled(True) - self.consoleMessage("Mesher started ...") - return + + # run locally + if profileName == 'local': + self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + if self.solver_object.Proxy.solver_process.waitForStarted(): + # Setting solve button to inactive to ensure that two instances of the same simulation aren't started + # simultaneously + self.form.pb_write_inp.setEnabled(False) + self.form.pb_run_solver.setEnabled(False) + self.form.terminateSolver.setEnabled(True) + self.consoleMessage("Mesher started ...") + return + + #run remotely + else: + pass + """ + remote_user = self.username + remote_hostname = self.hostname + + # create the ssh connection command + ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' ' + + # Get the working directory for the mesh + working_dir = self.output_path + #TODO: add filename to the path if selected + + # create the command to do the actual work + command = 'EOT \n' + command += 'cd ' + working_dir + '/meshCase \n' + command += './Allrun \n' + command += 'exit \n' + command += 'EOT' + command = ssh_prefix + ' << ' + command + + cmd = CfdTools.makeRunCommand(command,None) + """ + + #Mesh is ready to solve, so run it. else: self.Start = time.time() @@ -222,29 +394,110 @@ def runSolverProcess(self): # This is a workaround to emit code into macro without actually running it FreeCADGui.doCommand("proxy.running_from_macro = True") self.solver_object.Proxy.running_from_macro = False - FreeCADGui.doCommand( - "if proxy.running_from_macro:\n" + - " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + - " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + - " working_dir = CfdTools.getOutputPath(analysis_object)\n" + - " case_name = solver_object.InputCaseName\n" + - " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + - " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + - " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + - " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + - " env_vars = solver_runner.getRunEnvironment()\n" + - " solver_process = CfdConsoleProcess.CfdConsoleProcess(stdout_hook=solver_runner.process_output)\n" + - " solver_process.start(cmd, env_vars=env_vars)\n" + - " solver_process.waitForFinished()\n") - working_dir = CfdTools.getOutputPath(self.analysis_object) - case_name = self.solver_object.InputCaseName - solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) - cmd = self.solver_runner.get_solver_cmd(solver_directory) - env_vars = self.solver_runner.getRunEnvironment() - self.solver_object.Proxy.solver_process = CfdConsoleProcess(finished_hook=self.solverFinished, - stdout_hook=self.gotOutputLines, - stderr_hook=self.gotErrorLines) - self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + + # if running on local host + if self.profile_name == "local": + # This must be kept in one doCommand because of the if statement + FreeCADGui.doCommand( + "if proxy.running_from_macro:\n" + + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + + " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + + " working_dir = CfdTools.getOutputPath(analysis_object)\n" + + " case_name = solver_object.InputCaseName\n" + + " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + + " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + + " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + + " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + " env_vars = solver_runner.getRunEnvironment()\n" + + " solver_process = CfdConsoleProcess.CfdConsoleProcess(stdout_hook=solver_runner.process_output)\n" + + " solver_process.start(cmd,env_vars= env_vars)\n" + + " solver_process.waitForFinished()") + + working_dir = CfdTools.getOutputPath(self.analysis_object) + case_name = self.solver_object.InputCaseName + solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) + cmd = self.solver_runner.get_solver_cmd(solver_directory) + env_vars = self.solver_runner.getRunEnvironment() + self.solver_object.Proxy.solver_process = CfdConsoleProcess(finished_hook=self.solverFinished, + stdout_hook=self.gotOutputLines, + stderr_hook=self.gotErrorLines) + self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + + # running remotely + else: + #remote_user = self.username + #remote_hostname = self.hostname + + # create the ssh connection command + ssh_prefix = 'ssh -tt ' + self.username + '@' + self.hostname + ' ' + + # Get the working directory for the mesh + #working_dir = self.output_path + #TODO: add filename to the path if selected + + # create the command to do the actual work + #command = 'EOT \n' + #command += 'cd ' + working_dir + '/meshCase \n' + #command += './Allrun \n' + #command += 'exit \n' + #command += 'EOT' + #command = ssh_prefix + ' << ' + command + #cmd = CfdTools.makeRunCommand(command,None) + + # This must be kept in one doCommand because of the if statement + # The only difference between this command and the local command is " cmd = CfdTools.makeRunCommand('" + command + "',None)" + FreeCADGui.doCommand( + "if proxy.running_from_macro:\n" + + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + + " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + + " working_dir = CfdTools.getOutputPath(analysis_object)\n" + + " case_name = solver_object.InputCaseName\n" + + " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + + " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + + " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + + + # create the command to do the actual work + " ssh_prefix = 'ssh -tt ' + '" + self.username + "'+ '@' +'" + self.hostname + "'\n" + + " command = 'EOT \\n' \n" + + " command += 'cd ' + '" + self.working_dir + "' + '/case \\n' \n" + + " command += './Allrun \\n' \n" + + " command += 'exit \\n' \n" + + " command += 'EOT' \n" + + " command = ssh_prefix + ' << ' + command + '\\n' \n" + + " print('doCommand command:' + command) \n" + + " cmd = CfdTools.makeRunCommand(command,None)\n" + # was " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + " print('doCommand cmd:')\n" + + " print(cmd)\n" + + " env_vars = solver_runner.getRunEnvironment()\n" + + " solver_process = CfdConsoleProcess.CfdConsoleProcess(stdout_hook=solver_runner.process_output)\n" + + " solver_process.start(cmd,env_vars= env_vars)\n" + + " solver_process.waitForFinished()") + + # create the command to do the actual work + ssh_prefix = 'ssh -tt ' + self.username + '@' + self.hostname + command = 'EOT \n' + command += 'cd ' + self.working_dir + '/case \n' + command += './Allrun \n' + command += 'exit \n ' + command += 'EOT' + command = ssh_prefix + ' << ' + command + ' \n' + print("Code command:" + command) + cmd = CfdTools.makeRunCommand(command,None) # was " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + print("Code cmd:") + print(cmd) + + working_dir = CfdTools.getOutputPath(self.analysis_object) + case_name = self.solver_object.InputCaseName + solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) + + + #cmd = self.solver_runner.get_solver_cmd(solver_directory) + env_vars = self.solver_runner.getRunEnvironment() + self.solver_object.Proxy.solver_process = CfdConsoleProcess(finished_hook=self.solverFinished, + stdout_hook=self.gotOutputLines, + stderr_hook=self.gotErrorLines) + self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + if self.solver_object.Proxy.solver_process.waitForStarted(): # Setting solve button to inactive to ensure that two instances of the same simulation aren't started # simultaneously @@ -257,9 +510,11 @@ def runSolverProcess(self): self.consoleMessage("Error starting solver", 'Error') QApplication.restoreOverrideCursor() + def killSolverProcess(self): self.consoleMessage("Solver manually stopped") self.solver_object.Proxy.solver_process.terminate() + # TODO: kill the process on the server if we are running locally # Note: solverFinished will still be called def solverFinished(self, exit_code): diff --git a/Gui/CfdPreferencePage.ui b/Gui/CfdPreferencePage.ui index 3ecc267e..a62c6ee7 100644 --- a/Gui/CfdPreferencePage.ui +++ b/Gui/CfdPreferencePage.ui @@ -7,7 +7,7 @@ 0 0 560 - 909 + 1158 @@ -34,6 +34,19 @@ 6 + + + + true + + + The directory to which case folders are written. Used unless overridden on a per-analysis basis. + + + false + + + @@ -47,21 +60,21 @@ - - - - true - - - The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + + + 75 + true + - - false + + ParaView executable path - - + + true @@ -70,8 +83,8 @@ - - + + 75 @@ -79,12 +92,22 @@ - Default output directory + gmsh executable path - - + + + + true + + + ... + + + + + 75 @@ -92,7 +115,7 @@ - ParaView executable path + Default output directory @@ -109,11 +132,8 @@ - - - - true - + + ... @@ -129,39 +149,26 @@ - - + + true - The directory to which case folders are written. Used unless overridden on a per-analysis basis. + The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. false - - - - - 75 - true - - - - gmsh executable path - - - - - + + - ... + Add filename to output path ? @@ -417,14 +424,14 @@ Docker Container - + Use docker: - - - + + + @@ -446,7 +453,7 @@ - + diff --git a/Gui/CfdRemotePreferencePage.ui b/Gui/CfdRemotePreferencePage.ui new file mode 100644 index 00000000..34ee63a7 --- /dev/null +++ b/Gui/CfdRemotePreferencePage.ui @@ -0,0 +1,856 @@ + + + CfdRemotePreferencePage + + + + 0 + 0 + 488 + 1531 + + + + Remote Processing + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 12 + 75 + true + + + + + + + 1 + + + Use Remote Processing + + + + + + + + + + 1 + + + About Remote Processing + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + false + + + + + + + + 0 + 0 + + + + Add Profile + + + + + + + + 0 + 0 + + + + Delete Profile + + + + + + + + 75 + true + + + + Host profile: + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + + 75 + true + + + + Username on remote host: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Remote hostname or IP address: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Ping Remote Host + + + + + + + Test SSH + + + + + + + + + 8 + + + 8 + + + 8 + + + 0 + + + + + + 75 + true + + + + Meshing + + + Qt::AlignCenter + + + + + + + + 75 + true + + + + OpenFOAM + + + Qt::AlignCenter + + + + + + + + + 8 + + + 0 + + + 8 + + + 8 + + + + + Processes + + + + + + + Threads + + + + + + + + 0 + 0 + + + + + + + + Processes + + + + + + + Threads + + + + + + + + + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 14 + + + 6 + + + + + + 75 + true + + + + Remote output path + + + + + + + + + + true + + + The directory to which case folders are written. Used unless overridden on a per-analysis basis. + + + false + + + + + + + + 75 + true + + + + Remote OpenFOAM executable directory + + + + + + + ... + + + + + + + true + + + The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + false + + + + + + + true + + + The full path of the ParaView executable. Leave blank to use search path. + + + false + + + + + + + + 75 + true + + + + Remote gmsh executable path + + + + + + + true + + + ... + + + + + + + true + + + ... + + + + + + + true + + + ... + + + + + + + + 75 + true + + + + Remote ParaView executable path + + + + + + + Add filename to output path ? + + + + + + + + + 8 + + + 8 + + + 8 + + + 6 + + + 14 + + + 8 + + + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Remote Software dependencies + + + + + + + true + + + Run remote host dependency checker + + + + + + + + + + OpenFOAM + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + Install OpenFOAM + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + + + + + + ParaView + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + Install ParaView + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + + + + + + cfMesh + + + + + + + 0 + 0 + + + + URL: + + + + + + + Install cfMesh on remote host + + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + + + + + + + HiSA + + + + + + Install HiSA + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + + + + + + + Docker Container + + + + + + Use docker: + + + + + + + + + + Download from URL: + + + + + + + + + + Install Docker Container + + + + + + + + + + + + 8 + + + 5 + + + 8 + + + 8 + + + + + + 75 + true + + + + Output + + + + + + + + 0 + 0 + + + + + 0 + 120 + + + + QTextEdit::NoWrap + + + + + + + + + le_foam_dir + tb_choose_remote_foam_dir + le_paraview_path + tb_choose_paraview_path + le_gmsh_path + tb_choose_remote_gmsh_path + le_output_path + tb_choose_remote_output_dir + pb_run_dependency_checker + pb_download_install_openfoam + le_openfoam_url + tb_pick_openfoam_file + le_paraview_url + pb_download_install_paraview + tb_pick_paraview_file + le_cfmesh_url + pb_download_install_cfMesh + tb_pick_cfmesh_file + le_hisa_url + pb_download_install_hisa + tb_pick_hisa_file + + + + diff --git a/Gui/TaskPanelCfdMesh.ui b/Gui/TaskPanelCfdMesh.ui index 3a45b05c..fd907248 100644 --- a/Gui/TaskPanelCfdMesh.ui +++ b/Gui/TaskPanelCfdMesh.ui @@ -6,8 +6,8 @@ 0 0 - 510 - 856 + 512 + 1127 @@ -47,26 +47,6 @@ - - - - - 75 - true - - - - Mesh Parameters - - - - - - - Mesh utility: - - - @@ -112,6 +92,26 @@ + + + + Mesh utility: + + + + + + + + 75 + true + + + + Mesh Parameters + + + @@ -132,6 +132,42 @@ 0 + + + + Search + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 50 + false + + + + Point in mesh + + + @@ -156,37 +192,6 @@ 0 - - - - Edge detection - - - - - - - Implicit - - - - - - - Explicit - - - - - - - 1 - - - 100 - - - @@ -217,45 +222,40 @@ + + + + Edge detection + + + + + + + 1 + + + 100 + + + + + + + Implicit + + + + + + + Explicit + + + - - - - - 50 - false - - - - Point in mesh - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 5 - - - - - - - - Search - - - @@ -380,7 +380,52 @@ - + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + + 75 + true + + + + Host + + + + + + + + 0 + 0 + + + + + + + + 8 @@ -396,47 +441,34 @@ 8 - + Stop - + - Run mesher + Run mesh case - - - - - 75 - true - - + + - Meshing + Edit - + Write mesh case - - - - Edit - - - @@ -462,7 +494,7 @@ - Clear + Clear surface mesh diff --git a/Gui/TaskPanelCfdSolverControl.ui b/Gui/TaskPanelCfdSolverControl.ui index 1f003858..4543ded8 100644 --- a/Gui/TaskPanelCfdSolverControl.ui +++ b/Gui/TaskPanelCfdSolverControl.ui @@ -6,8 +6,8 @@ 0 0 - 366 - 563 + 612 + 1067 @@ -15,29 +15,20 @@ - - - 8 - - - 8 - - - 8 - - - 8 - - - 14 + + + + 75 + true + - - 6 + + OpenFOAM Solver - + - + 8 @@ -48,33 +39,22 @@ 8 - 6 + 8 14 - 8 + 6 - - - - Write - - - - - - - false - - - Edit - - - - + + + + 0 + 0 + + 75 @@ -82,14 +62,33 @@ - Case setup + Host + + + + + + + + 0 + 0 + + + + false + + + + + + -1 - + 8 @@ -108,43 +107,47 @@ 8 - + - false + true Stop - + - false + true - Run + Run OF case - - - - - 75 - true - + + + + true + + + Edit + + + + - Solver + Write OF case - + 8 @@ -155,66 +158,66 @@ 8 - 6 - - - 14 - - 8 - - - - false + + + + 8 - - Paraview + + 8 - - - - - - - 75 - true - + + 8 - - + + 6 - - - - - - - 75 - true - + + 14 - - Results + + 8 - + + + + + 75 + true + + + + Results + + + + + + + + 75 + true + + + + + + + + + + + true + + + Paraview + + + + - - - - - - 8 - - - 8 - - - 8 - - - 8 - diff --git a/InitGui.py b/InitGui.py index c3de69af..d480139e 100644 --- a/InitGui.py +++ b/InitGui.py @@ -31,6 +31,7 @@ def __init__(self): from CfdOF import CfdTools from PySide import QtCore from CfdOF.CfdPreferencePage import CfdPreferencePage + from CfdOF.CfdRemotePreferencePage import CfdRemotePreferencePage icon_path = os.path.join(CfdTools.getModulePath(), "Gui", "Icons", "cfd.svg") self.__class__.Icon = icon_path @@ -40,6 +41,7 @@ def __init__(self): icons_path = os.path.join(CfdTools.getModulePath(), "Gui", "Icons") QtCore.QDir.addSearchPath("icons", icons_path) FreeCADGui.addPreferencePage(CfdPreferencePage, "CfdOF") + FreeCADGui.addPreferencePage(CfdRemotePreferencePage, "CfdOF") def Initialize(self): # Must import QtCore in this function, not at the beginning of this file for translation support From 39cee45c556c9c7ec02398f4bb50bd419b5223b8 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Fri, 17 Mar 2023 14:13:09 -0600 Subject: [PATCH 10/20] modified: CfdOF/CfdPreferencePage.py modified: CfdOF/CfdRemotePreferencePage.py modified: CfdOF/Mesh/CfdMeshTools.py modified: CfdOF/Mesh/TaskPanelCfdMesh.py modified: Gui/CfdRemotePreferencePage.ui --- CfdOF/CfdPreferencePage.py | 10 +-- CfdOF/CfdRemotePreferencePage.py | 41 ++++++++++- CfdOF/Mesh/CfdMeshTools.py | 8 ++- CfdOF/Mesh/TaskPanelCfdMesh.py | 118 ++++++++++++++++++++++++------- Gui/CfdRemotePreferencePage.ui | 110 +++++++++++++++------------- 5 files changed, 205 insertions(+), 82 deletions(-) diff --git a/CfdOF/CfdPreferencePage.py b/CfdOF/CfdPreferencePage.py index 914d54f5..3a8cecbf 100644 --- a/CfdOF/CfdPreferencePage.py +++ b/CfdOF/CfdPreferencePage.py @@ -193,10 +193,12 @@ def loadSettings(self): self.setDownloadURLs() - if FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0): - self.form.cb_add_filename_to_output.setChecked(True) - else: - self.form.cb_add_filename_to_output.setChecked(False) + # disable add_filename_to_output for now. + self.form.cb_add_filename_to_output.setEnabled(False) + #if FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0): + # self.form.cb_add_filename_to_output.setChecked(True) + #else: + # self.form.cb_add_filename_to_output.setChecked(False) def consoleMessage(self, message="", colour_type=None): message = escape(message) diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py index 339780d3..2f33b378 100644 --- a/CfdOF/CfdRemotePreferencePage.py +++ b/CfdOF/CfdRemotePreferencePage.py @@ -213,6 +213,10 @@ def __init__(self): #connect the add filename to output control self.form.cb_add_filename_to_output.clicked.connect(self.addFilenameToOutputChanged) + self.form.cb_copy_back.clicked.connect(self.copyBackChanged) + self.form.cb_delete_remote_results.clicked.connect(self.deleteRemoteResultsChanged) + + # connect handlers for various control events # note: some of these aren't actually used. Code left in tact anyway @@ -274,6 +278,8 @@ def __init__(self): self.output_path = "" self.gmsh_path = "" self.add_filename_to_output = False + self.delete_remote_results = False + self.copy_back = False # TODO:fix these references # if they are still used. Most are not. @@ -320,15 +326,20 @@ def enableControls(self,value): self.form.pb_download_install_cfMesh.setEnabled(value) self.form.le_cfmesh_url.setEnabled(value) + self.form.cb_copy_back.setEnabled(value) + self.form.cb_delete_remote_results.setEnabled(value) # Controls below here are not yet operational regardless of if remote processing # is enabled or not. So they are disabled. Change this as new functionality # is added to the page value = False + # not implmented yet + self.form.cb_add_filename_to_output.setEnabled(value) + self.form.pb_about_remote_processing.setEnabled(value) - #disable the foam path chooser + # disable the foam path chooser self.form.tb_choose_remote_foam_dir.setEnabled(value) self.form.tb_choose_remote_foam_dir.setVisible(value) @@ -410,6 +421,8 @@ def loadProfile(self, profile_name): self.foam_dir = "" self.output_path = "" self.add_filename_to_output = False + self.copy_back = False + self.delete_remote_results = False else: hostPrefs = self.host_prefs_location @@ -422,8 +435,11 @@ def loadProfile(self, profile_name): self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") + self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") - #now set the control values + + #now set the UI controls self.form.le_hostname.setText(self.hostname) self.form.le_username.setText(self.username) self.form.le_mesh_processes.setText(str(self.mesh_processes)) @@ -433,6 +449,8 @@ def loadProfile(self, profile_name): self.form.le_foam_dir.setText(self.foam_dir) self.form.le_output_path.setText(self.output_path) self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + self.form.cb_copy_back.setChecked(self.copy_back) + self.form.cb_delete_remote_results.setChecked(self.delete_remote_results) # create a new profile and add it to the cb_profile control @@ -469,6 +487,8 @@ def addProfile(self): FreeCAD.ParamGet(hostPrefs).SetString("FoamDir", OPENFOAM_DIR) FreeCAD.ParamGet(hostPrefs).SetString("OutputPath","/tmp") FreeCAD.ParamGet(hostPrefs).SetBool("AddFilenameToOutput", False) + FreeCAD.ParamGet(hostPrefs).SetBool("CopyBack", False) + FreeCAD.ParamGet(hostPrefs).SetBool("DeleteRemoteResults", False) # now load the controls and local vars from the profile parameters self.loadProfile(profile_name) @@ -513,6 +533,7 @@ def profileChanged(self): # save the current profile by writing its the parameters + # not currently used. def saveProfile(self): print("saveProfile has fired") if self.profile_name != "": @@ -526,6 +547,8 @@ def saveProfile(self): FreeCAD.ParamGet(hostPrefs).SetString("FoamDir", self.foam_dir) FreeCAD.ParamGet(hostPrefs).SetString("OutputPath",self.output_path) FreeCAD.ParamGet(hostPrefs).SetBool("AddFilenameToOutput", self.add_filename_to_output) + FreeCAD.ParamGet(hostPrefs).SetBool("DeleteRemoteResults", self.delete_remote_results) + FreeCAD.ParamGet(hostPrefs).SetBool("CopyBack", self.copy_back) self.profileChanged = False def meshThreadsChanged(self): @@ -636,6 +659,20 @@ def addFilenameToOutputChanged(self): FreeCAD.ParamGet(self.host_prefs_location).SetBool("AddFilenameToOutput", self.add_filename_to_output) self.profile_changed = True + def copyBackChanged(self): + if self.profile_name == "": + return + self.copy_back = self.form.cb_copy_back.isChecked() + FreeCAD.ParamGet(self.host_prefs_location).SetBool("CopyBack", self.copy_back) + self.profile_changed = True + + def deleteRemoteResultsChanged(self): + if self.profile_name == "": + return + self.delete_remote_results = self.form.cb_delete_remote_results.isChecked() + FreeCAD.ParamGet(self.host_prefs_location).SetBool("DeleteRemoteResults", self.delete_remote_results) + self.profile_changed = True + #TODO: Move this to CfdTools def pingHost(self): self.consoleMessage("Performing ping test...") diff --git a/CfdOF/Mesh/CfdMeshTools.py b/CfdOF/Mesh/CfdMeshTools.py index a07d6979..bc189840 100644 --- a/CfdOF/Mesh/CfdMeshTools.py +++ b/CfdOF/Mesh/CfdMeshTools.py @@ -739,13 +739,19 @@ def writeMeshCase(self, host_profile): #if this is a remote mesh, copy the mesh case folder from the local mesh case dir # to the remote host's directory if host_profile != "local": + #this code is also used in reverse in TaskPanelCfdMesh.py for copyback after the meshing is done + # TODO: make it a function ? profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + host_profile remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") # rsync the meshCase directory to the remote host's output directory - # Typical useage: rsync -r --delete /tmp/meshCase me@david:/tmp + # Typical useage: rsync -r --remove-source-files --delete /tmp/meshCase me@david:/tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + # not using --remove-source-files because the workstation uses it to determine what actions are + # appropriate to do on the remote host, ie has a meshCase been written. try: CfdTools.runFoamCommand("rsync -r --delete " + self.meshCaseDir + " " + remote_user + "@" + remote_hostname + \ ":" + remote_output_path) diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 12bf5c3f..a7346049 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -24,26 +24,32 @@ # # LinuxGuy123@gmail.com's notes: # -# # TODOs, in addition to TODOs in the code itself # # - This code uses (dangerous) global vars to access things like profile_name, hostname, output dir, etc. # The profile_name, hostname, output dir should be attached to the mesh object and used from it. # #- Add use filename extension to the output path. For both local and remote processing. -# The value is already saved in prefs. You can get it with FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) +# The value is already saved in prefs for both local and hosts +# You can get it with FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) # Should be appended to the mesh object ? # -# - right now there is no way to edit the case on a remote host. This could be enabled by -# copying back the case to the local machine, allowing the user to edit the files in a temp dir -# and then copying them back to the remote host +#- the ParaView button gets enabled as soon as the mesh case is written before the results are +# available. This should be fixed. See updateUI. +# +#- the Clear Surface mesh button is enabled even when the surface isn't loaded. This should be fixed in +# updateUI. +# +#- the UI doesn't know where a meshCase is available to run. You can write a mesh case to the local, then +# change to a host and Run Mesh will be available to use. But there is no meshCase on that host yet. Ideally +# the mesh object would retain the host that the mesh case is on so that it could properly enable various actions. Luckily +# run mesh fails gracefully if no mesh case is found. # -#- copy the mesh back from the host to the local computer for Paraview, Load surface mesh and Check Mesh. +#- delete_remote_results removes the meshCase after a successful solve. Thus you have to rewrite the mesh case and rerun it on the +# server after every successful solve, if delete_remote_results is selected. +# +#- edit mesh should copy the edited mesh to the server after the edit is done. Right now it doesn't. # -#- enable check mesh for remote hosts -#- enable ParaView for remote hosts -#- enable Load Surface mesh for remote hosts -#- enable Clear Surface mesh for remote hosts from __future__ import print_function import FreeCAD @@ -59,7 +65,6 @@ if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore - from PySide import QtCore from PySide import QtGui from PySide.QtCore import Qt from PySide.QtGui import QApplication @@ -101,6 +106,8 @@ def __init__(self, obj): self.output_path = "" self.gmsh_path = "" self.add_filename_to_output = False + self.copy_back = False + self.delete_remote_results = False #add a local host to cb_profile self.form.cb_profile.addItem("local") @@ -193,19 +200,18 @@ def loadProfile(self, profile_name): self.form.pb_check_mesh.setEnabled(True) self.form.pb_load_mesh.setEnabled(True) self.form.pb_clear_mesh.setEnabled(True) - # the local code doesn't use these vars, so don't set them - # dangerous. - """ + + # the local code doesn't use these vars, setting them to be safe self.username = "" self.mesh_processes = 0 self.mesh_threads = 0 self.foam_processes = 0 self.foam_threads = 0 - self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") - self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + #self.foam_dir = FreeCAD.ParamGet(Prefs).GetString("FoamDir", "") + #self.output_path = FreeCAD.ParamGet(Prefs).GetString("OutputPath","") self.output_path = "" self.add_filename_to_output = False - """ + else: # set the vars to the remote host parameters # most of these aren't used, at least not in this page @@ -219,6 +225,8 @@ def loadProfile(self, profile_name): self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") + self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") #now set the control values self.mesh_obj.NumberOfProcesses = self.mesh_processes @@ -254,13 +262,11 @@ def profileChanged(self): print ("New profile is ", self.profile_name) self.loadProfile(self.profile_name) - # TODO enable and disable the appropriate controls here - # Remote hosts can't edit the case nor Paraview, check mesh, etc. - # Nor load surface mesh nor clear surface mesh. - #updateUI + # enable and disable the appropriate controls + self.updateUI # test routine to run a mesh without a proxy - # The real routine is runMesh way below + # The real routine is runMesh down below def runRemoteMesh(self): # run remote meshing directly, without a proxy #profile_prefs = CfdTools.getPreferencesLocation() + '/Hosts/' + self.profile_name @@ -335,15 +341,22 @@ def updateUI(self): self.form.snappySpecificProperties.setVisible(False) # enable the appropriate controls + # this is always an appropriate action self.form.pb_write_mesh.setEnabled(True) - if self.profile_name == 'local': + + #enable these if the mesh result is available locally + if self.profile_name == 'local' or self.copy_back : self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + # TODO This enables as soon as the mesh case is written. It shouldn't. self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + # TODO Should check that the mesh is loaded before enabling this. Not working right the way it is + self.form.pb_clear_mesh.setEnabled(True) - # remote host is being used + # remote host is being used without copy_back + # no local results to work on so disable these controls else: # remote hosts don't support these functions yet self.form.pb_run_mesh.setEnabled(True) @@ -483,14 +496,13 @@ def checkMeshClicked(self): " proxy.check_mesh_process.start(cmd, env_vars=env_vars)\n" + " proxy.check_mesh_process.waitForFinished()\n" + "else:\n" + - " proxy.mesh_process.start(cmd, env_vars=env_vars)"+ + #" proxy.mesh_process.start(cmd, env_vars=env_vars)"+ " proxy.check_mesh_process.start(cmd, env_vars=env_vars)") if self.mesh_obj.Proxy.mesh_process.waitForStarted(): self.form.pb_check_mesh.setEnabled(False) # Prevent user running a second instance self.form.pb_run_mesh.setEnabled(False) self.form.pb_write_mesh.setEnabled(False) - #self.form.pb_write_remote_mesh.setEnabled(False) self.form.pb_stop_mesh.setEnabled(False) self.form.pb_paraview.setEnabled(False) self.form.pb_load_mesh.setEnabled(False) @@ -520,7 +532,6 @@ def editMesh(self): CfdTools.openFileManager(case_path) def runMesh(self): - # TODO: this only works for local processing. Not remote processing. if CfdTools.getFoamRuntime() == "PosixDocker": CfdTools.startDocker() @@ -668,6 +679,59 @@ def meshFinished(self, exit_code): if exit_code == 0: self.consoleMessage('Meshing completed') self.analysis_obj.NeedsMeshRerun = False + + #check if there is work to do on the remote host + if self.profile_name != 'local': + # copy the meshcase back to the workstation? + # this code is also used in reverse in CfdMeshTools.py for copying the mesh case to the server + if self.copy_back: + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + local_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + + #case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) + # If this ^ is used as the destination dir, it will put the remote meshCase dir in the + # local meshCase directory, which is wrong (/tmp/CfdOF/meshCase/meshCase, for example + + # if we are deleting the mesh case on the server + # don't delete the mesh case. In most cases we need it for the solver to run later + # it will be deleted after the solve if we elect to remove results + + """ + if self.delete_remote_results: + deleteStr = "--remove-source-files " + else: + deleteStr = "" + """ + deleteStr = "" + + # rsync the meshCase result on the server to the workstation's output directory + # Typical useage: rsync -r --delete --remove-source-files me@david/tmp/meshCase /tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + + try: + CfdTools.runFoamCommand("rsync -r --delete " + deleteStr + remote_user + "@" + remote_hostname + ":" + remote_output_path + "/meshCase " + \ + local_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy mesh case back to local computer: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not copy mesh case back to local computer: " + str(e)) + else: + CfdTools.cfdMessage("Successfully copied mesh case to " + local_output_path + "\n" ) + if self.progressCallback: + self.progressCallback("Successfully copied mesh case to folder " + local_output_path + "\n") + if self.progressCallback: + self.progressCallback("Mesh case copy back process is complete.") + + # delete the result on the server ? + if self.delete_remote_results: + pass + else: self.consoleMessage("Meshing exited with error", 'Error') diff --git a/Gui/CfdRemotePreferencePage.ui b/Gui/CfdRemotePreferencePage.ui index 34ee63a7..f3aa1f28 100644 --- a/Gui/CfdRemotePreferencePage.ui +++ b/Gui/CfdRemotePreferencePage.ui @@ -356,21 +356,19 @@ 6 - - - - - 75 - true - - + + - Remote output path + ... - - + + + + Add filename to output path ? + + @@ -385,8 +383,8 @@ - - + + 75 @@ -394,27 +392,30 @@ - Remote OpenFOAM executable directory + Remote gmsh executable path - - + + + + true + ... - - - - true - - - The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + + + 75 + true + - - false + + Remote ParaView executable path @@ -431,21 +432,15 @@ - - - - - 75 - true - - + + - Remote gmsh executable path + Copy mesh and solver results back to workstation ? - - + + true @@ -454,18 +449,21 @@ - - + + true - - ... + + The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + false - - + + true @@ -474,8 +472,11 @@ - - + + + + + 75 @@ -483,14 +484,27 @@ - Remote ParaView executable path + Remote OpenFOAM executable directory - - + + + + + 75 + true + + - Add filename to output path ? + Remote output path + + + + + + + Delete mesh and server results on remote host ? From ea1e7a34f1a889e5597b2beaa689468d3610ebb4 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Sat, 18 Mar 2023 23:35:47 -0600 Subject: [PATCH 11/20] modified: CfdOF/CfdConsoleProcess.py modified: CfdOF/CfdRemotePreferencePage.py modified: CfdOF/CfdTools.py modified: CfdOF/Mesh/CfdMeshTools.py modified: CfdOF/Mesh/TaskPanelCfdMesh.py modified: CfdOF/Solve/CfdCaseWriterFoam.py modified: CfdOF/Solve/TaskPanelCfdSolverControl.py modified: Gui/TaskPanelCfdMesh.ui --- CfdOF/CfdConsoleProcess.py | 7 + CfdOF/CfdRemotePreferencePage.py | 21 ++- CfdOF/CfdTools.py | 1 - CfdOF/Mesh/CfdMeshTools.py | 14 +- CfdOF/Mesh/TaskPanelCfdMesh.py | 182 ++++++++++++++++++++--- CfdOF/Solve/CfdCaseWriterFoam.py | 16 +- CfdOF/Solve/TaskPanelCfdSolverControl.py | 152 ++++++++++++++----- Gui/TaskPanelCfdMesh.ui | 36 +++-- 8 files changed, 340 insertions(+), 89 deletions(-) diff --git a/CfdOF/CfdConsoleProcess.py b/CfdOF/CfdConsoleProcess.py index 980c77f2..abb06718 100644 --- a/CfdOF/CfdConsoleProcess.py +++ b/CfdOF/CfdConsoleProcess.py @@ -51,6 +51,7 @@ def __init__(self, finished_hook=None, stdout_hook=None, stderr_hook=None): def __del__(self): self.terminate() + #TODO document what the format of cmd is for developers def start(self, cmd, env_vars=None, working_dir=None): """ Start process and return immediately """ self.print_next_error_lines = 0 @@ -79,12 +80,18 @@ def terminate(self): self.process.write(b"terminate\n") self.process.waitForBytesWritten() # 'flush' else: + print("Process has been terminated") self.process.terminate() self.process.waitForFinished() def finished(self, exit_code): + print("Process has finished") if self.finishedHook: + #print("finished hook was called with exit code:" + str(exit_code)) self.finishedHook(exit_code) + else: + pass + #print("Error: finished wasn't hooked to a handler") def readStdout(self): # Ensure only complete lines are passed on diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py index 2f33b378..6f30cbe0 100644 --- a/CfdOF/CfdRemotePreferencePage.py +++ b/CfdOF/CfdRemotePreferencePage.py @@ -72,10 +72,15 @@ # I did not use worker threads to install cfMesh. I just ran the processes from the ssh command line. # # TODO: -# - get cfMesh URL control working -# - get About Remote Processing document window working + +# - fix downloadInstallCFMesh Its broken due to using profiles. Doesn't stop installing if a step fails. +# Also requires the working directory folder to be present. Fails if it isn't. +# - get About Remote Processing document window working - put User's Guide in it # - put the remote host computer fields in a container like Docker, OpenFOAM, etc. # - get tooltips working +# - OpenFOAM doesn't need the number of threads and processes like the meshers do. Remove one. + +# Done: get cfMesh URL control working # Done: implement host profiles so that multiple remote hosts can be used # Done: store and load the use remote processing boolean # Done: enable and disable all the controls with the cb_use_remote_processing checkbox result @@ -325,6 +330,7 @@ def enableControls(self,value): self.form.pb_download_install_cfMesh.setEnabled(value) self.form.le_cfmesh_url.setEnabled(value) + self.form.le_cfmesh_url.setText(CFMESH_URL) self.form.cb_copy_back.setEnabled(value) self.form.cb_delete_remote_results.setEnabled(value) @@ -728,7 +734,6 @@ def saveSettings(self): print("Error: saveSettings has been depreciated.") CfdTools.setRemoteFoamDir(self.foam_dir) CfdTools.setParaviewPath(self.paraview_path) - CfdTools.setRemoteGmshPath(self.gmsh_path) prefs = self.prefs_location FreeCAD.ParamGet(prefs).SetString("RemoteOutputPath", self.remote_output_dir) FreeCAD.ParamGet(prefs).SetBool("UseDocker",self.form.cb_docker_sel.isChecked()) @@ -1083,7 +1088,7 @@ def checkRemoteCfdDependencies(self): except Exception as e: #print(e) gmsh_msg = "Cannot run 'gmsh' on " + self.hostname + ". \n" - gmsh_msg += "Please install gmsh on the remote host." + gmsh_msg += "Please install gmsh on the remote host.\n" return_message += gmsh_msg print(gmsh_msg) @@ -1232,8 +1237,9 @@ def remoteDownloadInstallCfMesh(self): # If the user reruns the build after fully or partially building previously # this routine will fail. - # Get the username and hostname for the remote host + # TODO this routine assumes the output dir exists. Will fail if it doesn't. Fix this. + # Get the username and hostname for the remote host remote_user = self.username remote_hostname = self.hostname ssh_prefix = "ssh -tt " + remote_user + "@" + remote_hostname + " " @@ -1245,7 +1251,7 @@ def remoteDownloadInstallCfMesh(self): # cfMesh is installed in the user's home directory, not the output directory command = "EOT\n" - #command += "cd " + working_dir + "\n" + command += "cd " + working_dir + "\n" command += "mkdir cfMesh" + "\n" command += "exit \n" command += "EOT" @@ -1312,6 +1318,9 @@ def remoteDownloadInstallCfMesh(self): return + + + # old version, not used anymore. Doesn't handle remote install def downloadInstallCfMesh(self): print("Error:downloadInstallCFMesh has been depreciated.") diff --git a/CfdOF/CfdTools.py b/CfdOF/CfdTools.py index 8c3d4095..55a69c75 100644 --- a/CfdOF/CfdTools.py +++ b/CfdOF/CfdTools.py @@ -979,7 +979,6 @@ def makeRunCommand(cmd, dir, source_env=True): cmdline = ['bash', '-c', source + cd + cmd] return cmdline - def runFoamCommand(cmdline, case=None): """ Run a command in the OpenFOAM environment and wait until finished. Return output as (stdout, stderr, combined) diff --git a/CfdOF/Mesh/CfdMeshTools.py b/CfdOF/Mesh/CfdMeshTools.py index bc189840..34be9b11 100644 --- a/CfdOF/Mesh/CfdMeshTools.py +++ b/CfdOF/Mesh/CfdMeshTools.py @@ -732,9 +732,9 @@ def writeMeshCase(self, host_profile): os.chmod(fname, s.st_mode | stat.S_IEXEC) self.analysis.NeedsMeshRewrite = False - CfdTools.cfdMessage("Successfully wrote meshCase to local folder {}\n".format(self.meshCaseDir)) + CfdTools.cfdMessage("Wrote meshCase to local folder {}\n".format(self.meshCaseDir)) if self.progressCallback: - self.progressCallback("Successfully wrote meshCase to local folder {}\n".format(self.meshCaseDir)) + self.progressCallback("Wrote meshCase to local folder {}\n".format(self.meshCaseDir)) #if this is a remote mesh, copy the mesh case folder from the local mesh case dir # to the remote host's directory @@ -753,16 +753,16 @@ def writeMeshCase(self, host_profile): # not using --remove-source-files because the workstation uses it to determine what actions are # appropriate to do on the remote host, ie has a meshCase been written. try: - CfdTools.runFoamCommand("rsync -r --delete " + self.meshCaseDir + " " + remote_user + "@" + remote_hostname + \ + CfdTools.runFoamCommand("rsync -r --delete --remove-source-files " + self.meshCaseDir + " " + remote_user + "@" + remote_hostname + \ ":" + remote_output_path) except Exception as e: - CfdTools.cfdMessage("Could not copy meshCase to remote host: " + str(e)) + CfdTools.cfdMessage("Could not move mesh case to remote host: " + str(e)) if self.progressCallback: - self.progressCallback("Could not copy meshCase to remote host: " + str(e)) + self.progressCallback("Could not move mesh case to remote host: " + str(e)) else: - CfdTools.cfdMessage("Successfully copied local meshCase to folder " + remote_output_path + " on remote host " + remote_hostname + "\n" ) + CfdTools.cfdMessage("Moved mesh case to " + remote_hostname + ":" + remote_output_path + "\n" ) if self.progressCallback: - self.progressCallback("Successfully copied local meshCase to folder " + remote_output_path + " on remote host " + remote_hostname + "\n") + self.progressCallback("Moved mesh case to " + remote_hostname + ":" + remote_output_path + "\n") if self.progressCallback: self.progressCallback("Mesh case write process is complete.") diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index a7346049..10b53612 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -26,30 +26,50 @@ # # TODOs, in addition to TODOs in the code itself # -# - This code uses (dangerous) global vars to access things like profile_name, hostname, output dir, etc. +# -This code uses (dangerous) global vars to access things like profile_name, hostname, output dir, etc. # The profile_name, hostname, output dir should be attached to the mesh object and used from it. # -#- Add use filename extension to the output path. For both local and remote processing. +# -Add use filename extension to the output path. For both local and remote processing. # The value is already saved in prefs for both local and hosts # You can get it with FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) # Should be appended to the mesh object ? # -#- the ParaView button gets enabled as soon as the mesh case is written before the results are +# -the ParaView button gets enabled as soon as the mesh case is written before the results are # available. This should be fixed. See updateUI. # -#- the Clear Surface mesh button is enabled even when the surface isn't loaded. This should be fixed in +# -the Clear Surface mesh button is enabled even when the surface isn't loaded. This should be fixed in # updateUI. # -#- the UI doesn't know where a meshCase is available to run. You can write a mesh case to the local, then +# -the UI doesn't know where a meshCase is available to run. You can write a mesh case to the local, then # change to a host and Run Mesh will be available to use. But there is no meshCase on that host yet. Ideally # the mesh object would retain the host that the mesh case is on so that it could properly enable various actions. Luckily # run mesh fails gracefully if no mesh case is found. # -#- delete_remote_results removes the meshCase after a successful solve. Thus you have to rewrite the mesh case and rerun it on the -# server after every successful solve, if delete_remote_results is selected. +# -delete_remote_results removes the meshCase after a successful solve. Actually, I disabled this. Thus you (would) have to rewrite +# the mesh case and rerun it on the server after every successful solve, if delete_remote_results is selected. # -#- edit mesh should copy the edited mesh to the server after the edit is done. Right now it doesn't. +# -edit mesh should copy the edited mesh to the server after the edit is done. Right now it doesn't. # +# -remote meshing has not been tested in macros. +# +# -cfMesh runs only 1 mesh process on the remote machine if 1,0 is selected for processes, threads. If you run 8 threads, for example, it +# doesn't ever finish +# the meshing process stack on the remote host is this. Looks legit, but never finishes. +# +# $ ps aux | grep Mesh +#me 134162 0.0 0.0 174292 17336 ? Sl 21:58 0:00 mpiexec -np 8 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134163 0.0 0.0 221328 944 ? S 21:58 0:00 tee -a log.cartesianMesh +#me 134164 0.0 0.0 221328 892 ? S 21:58 0:00 tee -a log.cartesianMesh +#me 134168 102 0.3 4034732 237604 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134169 102 0.3 4038380 231280 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134170 102 0.3 4029584 221532 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134171 102 0.3 4063928 258864 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134172 102 0.3 4035376 234968 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134173 102 0.3 4032120 233276 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134174 102 0.3 4030184 221800 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134175 102 0.3 4066644 257484 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel + + from __future__ import print_function import FreeCAD @@ -143,6 +163,8 @@ def __init__(self, obj): self.form.pb_clear_mesh.clicked.connect(self.pbClearMeshClicked) self.form.pb_searchPointInMesh.clicked.connect(self.searchPointInMesh) self.form.pb_check_mesh.clicked.connect(self.checkMeshClicked) + self.form.pb_copy_to_host.clicked.connect(self.copyMeshcaseToHost) + self.form.pb_delete_mesh.clicked.connect(self.deleteMeshcase) self.radioGroup = QtGui.QButtonGroup() self.radioGroup.addButton(self.form.radio_explicit_edge_detection) @@ -211,6 +233,8 @@ def loadProfile(self, profile_name): #self.output_path = FreeCAD.ParamGet(Prefs).GetString("OutputPath","") self.output_path = "" self.add_filename_to_output = False + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir + self.form.pb_delete_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) else: # set the vars to the remote host parameters @@ -239,6 +263,12 @@ def loadProfile(self, profile_name): self.form.pb_load_mesh.setEnabled(False) self.form.pb_clear_mesh.setEnabled(False) + # enable if using a remote host + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir + self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + # TODO should check if there is a mesh case on the remote host and set accordingly + self.form.pb_delete_mesh.setEnabled(True) + #TODO: fix these, if we need to. #self.form.le_mesh_processes.setText(str(self.mesh_processes)) #self.form.le_mesh_threads.setText(str(self.mesh_threads)) @@ -274,18 +304,19 @@ def runRemoteMesh(self): remote_hostname = self.hostname # create the ssh connection command - ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' ' + # was ssh -tt + ssh_prefix = 'ssh -t ' + remote_user + '@' + remote_hostname + ' ' # Get the working directory for the mesh working_dir = self.output_path #TODO: add filename to the path if selected # create the command to do the actual work - command = 'EOT \n' - command += 'cd ' + working_dir + '/meshCase \n' + #command = 'EOT \n' + command = 'cd ' + working_dir + '/meshCase \n' command += './Allmesh \n' command += 'exit \n' - command += 'EOT' + command += 'EOT \n' command = ssh_prefix + ' << ' + command self.consoleMessage("Starting remote meshing...") @@ -344,20 +375,30 @@ def updateUI(self): # this is always an appropriate action self.form.pb_write_mesh.setEnabled(True) - #enable these if the mesh result is available locally + #enable these if the mesh is available locally if self.profile_name == 'local' or self.copy_back : + self.form.pb_copy_to_host.setEnabled(False) + self.form.pb_delete_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) - self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + # TODO have to enable this for the case that there is no local meshcase but + # we have written one to a remote host We should be checking if the case is available on + # the remote host, not the local host + #self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + self.form.pb_run_mesh.setEnabled(True) + # TODO This enables as soon as the mesh case is written. It shouldn't. self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) # TODO Should check that the mesh is loaded before enabling this. Not working right the way it is - self.form.pb_clear_mesh.setEnabled(True) + self.form.pb_clear_mesh.setEnabled(True) # remote host is being used without copy_back # no local results to work on so disable these controls else: + self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + #TODO should check if the mesh is present on the remote host + self.form.pb_delete_mesh.setEnabled(True) # remote hosts don't support these functions yet self.form.pb_run_mesh.setEnabled(True) self.form.pb_paraview.setEnabled(False) @@ -609,7 +650,8 @@ def runMesh(self): #FreeCADGui.doCommand("print('hostname:' + remote_hostname)") # create the ssh connection command - FreeCADGui.doCommand("ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' '") + # was ssh -tt + FreeCADGui.doCommand("ssh_prefix = 'ssh -t ' + remote_user + '@' + remote_hostname + ' '") # Get the working directory for the mesh FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") @@ -718,17 +760,20 @@ def meshFinished(self, exit_code): CfdTools.runFoamCommand("rsync -r --delete " + deleteStr + remote_user + "@" + remote_hostname + ":" + remote_output_path + "/meshCase " + \ local_output_path) except Exception as e: - CfdTools.cfdMessage("Could not copy mesh case back to local computer: " + str(e)) + CfdTools.cfdMessage("Could not copy mesh to local computer: " + str(e)) if self.progressCallback: - self.progressCallback("Could not copy mesh case back to local computer: " + str(e)) + self.progressCallback("Could not copy mesh to local computer: " + str(e)) else: - CfdTools.cfdMessage("Successfully copied mesh case to " + local_output_path + "\n" ) + CfdTools.cfdMessage("Copied mesh case to " + local_output_path + " on local computer\n" ) if self.progressCallback: - self.progressCallback("Successfully copied mesh case to folder " + local_output_path + "\n") + self.progressCallback("Copied mesh case to " + local_output_path + "on local computer\n") if self.progressCallback: self.progressCallback("Mesh case copy back process is complete.") # delete the result on the server ? + # if this got deleted then we'd have to remesh every run + # or copy a mesh from the local machine to the server + # not implemented for this reason if self.delete_remote_results: pass @@ -741,6 +786,103 @@ def meshFinished(self, exit_code): self.pbClearMeshClicked() self.updateUI() + + # Delete the mesh case on the current host, including the local host if it is the current host + # TODO: Add a warning for the user so they can cancel if desired + def deleteMeshcase(self): + # local host + if self.hostname == 'local': + #TODO Check if the meshCase exists + local_prefs = CfdTools.getPreferencesLocation() + local_output_path = FreeCAD.ParamGet(local_prefs).GetString("DefaultOutputPath","") + print("Local output path:" + local_output_path) + + + # create the command to do the actual work + # TODO This won't work on a Windows or Mac machine Fix it + command = "rm -rf " + local_output_path + "/meshCase" + + try: + CfdTools.runFoamCommand(command, "./") + + except Exception as e: + CfdTools.cfdMessage("Could not delete the local mesh case:" + str(e)) + self.consoleMessage("Could not delete the local mesh case:" + str(e)) + else: + CfdTools.cfdMessage("Deleted the mesh case in " + local_output_path + "\n" ) + self.consoleMessage("Deleted the mesh case in " + local_output_path + "\n" ) + # now update the UI + self.updateUI + + # remote host + # TODO uses a combination of looked up parameters and local vars ! Very dangerous. Fix this + # TODO check if mesh case exists + else: + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + #remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + + # create the command to do the actual work + command = 'ssh -tt ' + self.username + '@' + self.hostname # was -tt + #command += ' << EOT \n' + command = 'cd ' + remote_output_path + '\n' + command += 'rm -rf meshCase ' + '\n' + command += 'exit \n ' + command += 'EOT \n' + #print("Code command:" + command) + + try: + CfdTools.runFoamCommand(command) + + except Exception as e: + CfdTools.cfdMessage("Could not delete mesh case on remote host:" + str(e)) + self.consoleMessage("Could not delete mesh case on remote host:" + str(e)) + else: + CfdTools.cfdMessage("Deleted mesh case in " + remote_hostname + ":" + remote_output_path + "\n" ) + self.consoleMessage("Deleted mesh case in " + remote_hostname + ":" + remote_output_path + "\n" ) + # now update the UI + self.updateUI + + + + def copyMeshcaseToHost(self): + #check that a host is selected + if self.profile_name == 'local': + self.consoleMessage("Select a host to copy the mesh case to.") + else: + # copy the meshcase back to the workstation + # this code is also used in reverse in CfdMeshTools.py for copying the mesh case to the server + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + local_output_path = FreeCAD.ParamGet(local_prefs).GetString("DefaultOutputPath","") + + #case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) + # If this ^ is used as the destination dir, it will put the remote meshCase dir in the + # local meshCase directory, which is wrong (/tmp/CfdOF/meshCase/meshCase, for example + + # rsync the meshCase result on the server to the workstation's output directory + # Typical useage: rsync -r --delete --remove-source-files me@david/tmp/meshCase /tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + + try: + CfdTools.runFoamCommand("rsync -r --delete " + local_output_path + "/meshCase " + remote_user + "@" + remote_hostname + ":" \ + + remote_output_path) + + except Exception as e: + CfdTools.cfdMessage("Could not copy mesh case to host: " + str(e)) + self.consoleMessage("Could not copy mesh case to host: " + str(e)) + else: + CfdTools.cfdMessage("Copied mesh case to " + remote_hostname + ":" + remote_output_path + "\n" ) + self.consoleMessage("Copied mesh case to " + remote_hostname + ":" + remote_output_path + "\n" ) + + def openParaview(self): QApplication.setOverrideCursor(Qt.WaitCursor) case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) diff --git a/CfdOF/Solve/CfdCaseWriterFoam.py b/CfdOF/Solve/CfdCaseWriterFoam.py index ca6e2270..e6d20972 100644 --- a/CfdOF/Solve/CfdCaseWriterFoam.py +++ b/CfdOF/Solve/CfdCaseWriterFoam.py @@ -154,7 +154,7 @@ def writeCase(self, profile_name): cfdMessage("Successfully wrote case to folder {}\n".format(self.working_dir)) if self.progressCallback: - self.progressCallback("Case written locally successfully") + self.progressCallback("Case written locally") # if using a remote host, copy the case folder from the local case dir # to the remote host's directory @@ -172,18 +172,22 @@ def writeCase(self, profile_name): # rsync the meshCase directory to the remote host's output directory # Typical useage: rsync -r --delete /tmp/ me@david:/tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + # + try: - CfdTools.runFoamCommand("rsync -r --delete " + self.case_folder + " " + remote_user + "@" + remote_hostname + \ + CfdTools.runFoamCommand("rsync -r --delete --remove-source-files " + self.case_folder + " " + remote_user + "@" + remote_hostname + \ ":" + remote_output_path) except Exception as e: - CfdTools.cfdMessage("Could not copy case to remote host: " + str(e)) + CfdTools.cfdMessage("Could not move case to remote host: " + str(e)) if self.progressCallback: - self.progressCallback("Could not copy case to remote host: " + str(e)) + self.progressCallback("Could not move case to remote host: " + str(e)) return False else: - CfdTools.cfdMessage("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n" ) + CfdTools.cfdMessage("Moved solver case to " + remote_hostname + ":" + remote_output_path + "\n" ) if self.progressCallback: - self.progressCallback("Successfully copied local case to folder " + remote_output_path + " on remote host " + remote_hostname + "\n") + self.progressCallback("Moved solver case to " + remote_hostname + ":" + remote_output_path + "\n") return True diff --git a/CfdOF/Solve/TaskPanelCfdSolverControl.py b/CfdOF/Solve/TaskPanelCfdSolverControl.py index 364131ea..cddbbc50 100644 --- a/CfdOF/Solve/TaskPanelCfdSolverControl.py +++ b/CfdOF/Solve/TaskPanelCfdSolverControl.py @@ -35,23 +35,30 @@ from PySide.QtCore import Qt from PySide.QtGui import QApplication +# ******************************************************************************************************** # LinuxGuy123's Notes # # TODOs (there are some in the code as well) # -#- add filename to the output path. (addFilenameToOutput) For both local and remote useRemoteProcessing. +# -add filename to the output path. (addFilenameToOutput) For both local and remote useRemoteProcessing. # It is already saved in prefs, for both local and remote hosts. You can get it with # FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) # -#- copy the mesh back to the local computer for Edit and Paraview buttons +# -check on the number of cores that are being asked for and used by OpenFOAM # -#- makeRunCommand is using the local OF install dir to build the remote run command. It works but it isn't correct. -# The remote run is not calling the source command to set up OF usage. It is relying on the shell to do that, through -# bashrc and OF working directly from the command line. +# -makeRunCommand is using the local OF bash command to build the remote run command. It works but it isn't correct. +# The remote run is not calling the source command to set up OF usage. It is relying on the bash shell on the remote host +# to do that, through bashrc and OF working directly from the command line. # -#- enable and disable buttons appropriately when running and when done +# -enable and disable buttons appropriately when running and when done # # global vars are used for stuff that should be passed via the solver object. +# +# -edit case doesn't copy the case back to the server after the edit is done. +# +# -remote solving has not been tested in macros +# +#- presently does not set the number of threads that the solver uses properly. Must be done manually. class TaskPanelCfdSolverControl: def __init__(self, solver_runner_obj): @@ -163,7 +170,7 @@ def loadProfile(self, profile_name): self.add_filename_to_output = False """ else: - #set the vars to the remote host parameters + # set the vars to the remote host parameters # most of these aren't used, at least not in this page hostPrefs = self.host_prefs_location self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") @@ -174,13 +181,22 @@ def loadProfile(self, profile_name): #self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + + # these are used self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") + self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") + + + # now set the control values + # leaving these in in case we pass parameters like these to the solver + # object some day. - #now set the control values #self.mesh_obj.NumberOfProcesses = self.mesh_processes #self.mesh_obj.NumberOfThreads = self.mesh_threads #TODO: fix these, if we need to. + # Leaving these in in case we add controls to set these someday #self.form.le_mesh_processes.setText(str(self.mesh_processes)) #self.form.le_mesh_threads.setText(str(self.mesh_threads)) @@ -212,7 +228,15 @@ def profileChanged(self): def updateUI(self): solverDirectory = os.path.join(self.working_dir, self.solver_object.InputCaseName) - self.form.pb_edit_inp.setEnabled(os.path.exists(solverDirectory)) + + if self.profile_name == 'local': + self.form.pb_edit_inp.setEnabled(os.path.exists(solverDirectory)) + + # TODO: enable local editing of the solver case + else: + self.form.pb_edit_inp.setEnabled(False) + + # TODO: Paraview is enabled even though the solver hasn't been run yet. Fix this ? self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(solverDirectory, "pv.foam"))) self.form.pb_run_solver.setEnabled(os.path.exists(os.path.join(solverDirectory, "Allrun"))) @@ -299,6 +323,7 @@ def runSolverProcess(self, profileName): self.Start = time.time() # Check for changes that require remesh + # TODO: This will not run the mesher on a remote host. Fix this ? if FreeCAD.GuiUp and ( self.analysis_object.NeedsMeshRewrite or self.analysis_object.NeedsCaseRewrite or @@ -357,15 +382,19 @@ def runSolverProcess(self, profileName): self.consoleMessage("Mesher started ...") return - #run remotely + # run remotely + # not implemented else: - pass + self.consoleMessage("Meshing from within the solver is not implemented for remote hosts.") + self.consoleMessage("Generate the mesh from the mesh object instead.") + return """ remote_user = self.username remote_hostname = self.hostname # create the ssh connection command - ssh_prefix = 'ssh -tt ' + remote_user + '@' + remote_hostname + ' ' + # use ssh -t, not ssh -tt + ssh_prefix = 'ssh -t' + remote_user + '@' + remote_hostname + ' ' # Get the working directory for the mesh working_dir = self.output_path @@ -376,7 +405,7 @@ def runSolverProcess(self, profileName): command += 'cd ' + working_dir + '/meshCase \n' command += './Allrun \n' command += 'exit \n' - command += 'EOT' + command += 'EOT \n' command = ssh_prefix + ' << ' + command cmd = CfdTools.makeRunCommand(command,None) @@ -425,27 +454,10 @@ def runSolverProcess(self, profileName): # running remotely else: - #remote_user = self.username - #remote_hostname = self.hostname - - # create the ssh connection command - ssh_prefix = 'ssh -tt ' + self.username + '@' + self.hostname + ' ' - - # Get the working directory for the mesh - #working_dir = self.output_path - #TODO: add filename to the path if selected - - # create the command to do the actual work - #command = 'EOT \n' - #command += 'cd ' + working_dir + '/meshCase \n' - #command += './Allrun \n' - #command += 'exit \n' - #command += 'EOT' - #command = ssh_prefix + ' << ' + command - #cmd = CfdTools.makeRunCommand(command,None) # This must be kept in one doCommand because of the if statement # The only difference between this command and the local command is " cmd = CfdTools.makeRunCommand('" + command + "',None)" + # TODO: Test the macro code. FreeCADGui.doCommand( "if proxy.running_from_macro:\n" + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + @@ -456,10 +468,11 @@ def runSolverProcess(self, profileName): " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + - # create the command to do the actual work - " ssh_prefix = 'ssh -tt ' + '" + self.username + "'+ '@' +'" + self.hostname + "'\n" + - " command = 'EOT \\n' \n" + - " command += 'cd ' + '" + self.working_dir + "' + '/case \\n' \n" + + # create the command to do the actual work + # was ssh -tt but then the shell wouldn't exit + " ssh_prefix = 'ssh -t ' + '" + self.username + "'+ '@' +'" + self.hostname + "'\n" + + " #command = 'EOT \\n' \n" + + " command = 'cd ' + '" + self.working_dir + "' + '/case \\n' \n" + " command += './Allrun \\n' \n" + " command += 'exit \\n' \n" + " command += 'EOT' \n" + @@ -473,18 +486,37 @@ def runSolverProcess(self, profileName): " solver_process.start(cmd,env_vars= env_vars)\n" + " solver_process.waitForFinished()") + + """ + # This was used for testing # create the command to do the actual work ssh_prefix = 'ssh -tt ' + self.username + '@' + self.hostname command = 'EOT \n' command += 'cd ' + self.working_dir + '/case \n' command += './Allrun \n' command += 'exit \n ' - command += 'EOT' + command += 'EOT \n' command = ssh_prefix + ' << ' + command + ' \n' print("Code command:" + command) - cmd = CfdTools.makeRunCommand(command,None) # was " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + cmd = CfdTools.makeRunCommand(command,None) # was cmd = solver_runner.get_solver_cmd(solver_directory)\n" + print("Code cmd:") print(cmd) + """ + + # create the command to do the actual work + command = 'ssh -t ' + self.username + '@' + self.hostname # was -tt + command += '<< EOT \n' + command += ' cd ' + self.working_dir + '/case \n' + command += './Allrun \n' + command += 'exit \n ' + command += 'EOT \n' + print("Code command:" + command) + + #cmd = ['bash', '-c', command] + #print("Code cmd:") + #print(cmd) + + cmd = CfdTools.makeRunCommand(command,None) working_dir = CfdTools.getOutputPath(self.analysis_object) case_name = self.solver_object.InputCaseName @@ -519,7 +551,51 @@ def killSolverProcess(self): def solverFinished(self, exit_code): if exit_code == 0: - self.consoleMessage("Simulation finished successfully") + self.consoleMessage("Simulation finished") + + #check if there is work to do on the remote host + if self.profile_name != 'local': + # copy the solver case back to the workstation? + # this code is also used in TaskPanelCfdMesh.py for copying the mesh case to the workstation + if self.copy_back: + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + local_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + + # if we are deleting the solver and mesh case on the server + # if we delete the mesh case we'll need to remesh before running the solver + + if self.delete_remote_results: + deleteStr = "--remove-source-files " + else: + deleteStr = "" + + # rsync the solver case result on the server to the workstation's output directory + # Typical useage: rsync -r --delete --remove-source-files me@david/tmp/case /tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + + try: + CfdTools.runFoamCommand("rsync -r --delete " + deleteStr + remote_user + "@" + remote_hostname + ":" + remote_output_path + "/case " + \ + local_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy solver case back to local computer: " + str(e)) + self.consoleMessage("Could not copy solver case back to local computer: " + str(e)) + + else: + CfdTools.cfdMessage("Copied solver case to " + local_output_path + "\n" ) + self.consoleMessage("Copied solver case to " + local_output_path + "\n" ) + + # the mesh case is still on the server + # delete the mesh case result on the server ? + # for now we'll leave it there + if self.delete_remote_results: + pass + else: self.consoleMessage("Simulation exited with error", 'Error') self.solver_runner.solverFinished() diff --git a/Gui/TaskPanelCfdMesh.ui b/Gui/TaskPanelCfdMesh.ui index fd907248..da79d20e 100644 --- a/Gui/TaskPanelCfdMesh.ui +++ b/Gui/TaskPanelCfdMesh.ui @@ -425,7 +425,7 @@ - + 8 @@ -441,31 +441,45 @@ 8 - - + + - Stop + Run mesh case - - + + - Run mesh case + Write mesh case - Edit + Edit mesh case - - + + - Write mesh case + Stop + + + + + + + Copy local mesh case to host + + + + + + + Delete mesh case From 6eba9bce21393a3d4adde2acb5b086c14c237182 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Sun, 19 Mar 2023 13:59:00 -0600 Subject: [PATCH 12/20] Added add filename to output --- CfdOF/CfdTools.py | 61 +++++++++++++++++++++--- CfdOF/Mesh/CfdMeshTools.py | 3 +- CfdOF/Mesh/TaskPanelCfdMesh.py | 42 ++++++++++------ CfdOF/Solve/TaskPanelCfdSolverControl.py | 25 ++++++++-- Gui/TaskPanelCfdMesh.ui | 29 ++++++----- Gui/TaskPanelCfdSolverControl.ui | 25 ++++++---- 6 files changed, 137 insertions(+), 48 deletions(-) diff --git a/CfdOF/CfdTools.py b/CfdOF/CfdTools.py index 55a69c75..967c7d22 100644 --- a/CfdOF/CfdTools.py +++ b/CfdOF/CfdTools.py @@ -40,6 +40,7 @@ from datetime import timedelta import FreeCAD from FreeCAD import Units +#import App import Part import BOPTools from BOPTools import SplitFeatures @@ -88,16 +89,59 @@ docker_container = None -def getDefaultOutputPath(): - prefs = getPreferencesLocation() - output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") - if not output_path: - output_path = tempfile.gettempdir() - output_path = os.path.normpath(output_path) - return output_path + +# Modified by LinuxGuy to handle add_filename_to_path +# This is called default because it is used in the mesh and solver objects +# but can be over ridden by the user +def getDefaultOutputPath(profile = 'local'): + + # host is local + if profile == 'local': + prefs = getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + if not output_path: + output_path = tempfile.gettempdir() + print("Root output path:" + output_path) + + add_filename = FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput") + print("AddFilenameToOutput:" + str(add_filename)) + + if add_filename: + #TODO: might want to warn the user if they haven't saved the filename yet + App = FreeCAD + filename = App.ActiveDocument.Name + if filename: + output_path += "/" + filename + + output_path = os.path.normpath(output_path) + print("Output path:" + output_path) + return output_path + + #remote case + else: + prefs = getPreferencesLocation() + hostprefs = prefs +"/Hosts/" + profile + output_path = FreeCAD.ParamGet(hostprefs).GetString("OutputPath", "") + + if not output_path: + #TODO: might want to warn the user if they haven't saved the filename yet + output_path = tempfile.gettempdir() + + if FreeCAD.ParamGet(hostprefs).GetBool("AddFilenameToOutput"): + App = FreeCAD + filename = App.ActiveDocument.Name + if filename: + output_path += "/" + filename + # the local computer might be Windows and the remote one Linux + # so don't normalize the path to the local OS + #output_path = os.path.normpath(output_path) + print("Output path:" + output_path) + return output_path + # TODO: Is this used anymore ? def getDefaultRemoteOutputPath(): + print("Error: getDefaultRemoteOutputPath is depreciated.") prefs = getPreferencesLocation() output_path = FreeCAD.ParamGet(prefs).GetString("DefaultRemoteOutputPath", "") if not output_path: @@ -504,6 +548,7 @@ def getPreferencesLocation(): # Set parameter location return "User parameter:BaseApp/Preferences/Mod/CfdOF" + def setFoamDir(installation_path): prefs = getPreferencesLocation() # Set OpenFOAM install path in parameters @@ -725,6 +770,8 @@ def getRemoteGmshPath(): return gmsh_path + + def translatePath(p): """ Transform path to the perspective of the Linux subsystem in which OpenFOAM is run (e.g. mingw) diff --git a/CfdOF/Mesh/CfdMeshTools.py b/CfdOF/Mesh/CfdMeshTools.py index 34be9b11..9c652658 100644 --- a/CfdOF/Mesh/CfdMeshTools.py +++ b/CfdOF/Mesh/CfdMeshTools.py @@ -744,7 +744,8 @@ def writeMeshCase(self, host_profile): profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + host_profile remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(host_profile) # rsync the meshCase directory to the remote host's output directory # Typical useage: rsync -r --remove-source-files --delete /tmp/meshCase me@david:/tmp diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 10b53612..91aa3c19 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -233,6 +233,7 @@ def loadProfile(self, profile_name): #self.output_path = FreeCAD.ParamGet(Prefs).GetString("OutputPath","") self.output_path = "" self.add_filename_to_output = False + # Not sure if this causes a side effect or not. case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir self.form.pb_delete_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) @@ -247,7 +248,9 @@ def loadProfile(self, profile_name): self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") - self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + + #self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = CfdTools.getDefaultOutputPath(self.profile_name) self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") @@ -396,7 +399,9 @@ def updateUI(self): # remote host is being used without copy_back # no local results to work on so disable these controls else: - self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + #self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + local_dir = CfdTools.getDefaultOutputPath('local') + self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(local_dir, "meshCase"))) #TODO should check if the mesh is present on the remote host self.form.pb_delete_mesh.setEnabled(True) # remote hosts don't support these functions yet @@ -654,10 +659,10 @@ def runMesh(self): FreeCADGui.doCommand("ssh_prefix = 'ssh -t ' + remote_user + '@' + remote_hostname + ' '") # Get the working directory for the mesh - FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") + #FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") + FreeCADGui.doCommand("working_dir = CfdTools.getDefaultOutputPath('" + self.profile_name + "' )") #FreeCADGui.doCommand("print('working directory:' + working_dir)") - # create the command to do the actual work FreeCADGui.doCommand("command = 'EOT \\n' \n" + "command += 'cd ' + working_dir + '/meshCase \\n' \n" + @@ -718,6 +723,9 @@ def gotErrorLines(self, lines): self.consoleMessage(print_err, 'Error') def meshFinished(self, exit_code): + if self.form.cb_notify.isChecked(): + #print("Beeping now") + QApplication.beep() if exit_code == 0: self.consoleMessage('Meshing completed') self.analysis_obj.NeedsMeshRerun = False @@ -732,8 +740,8 @@ def meshFinished(self, exit_code): remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") - local_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + local_output_path = CfdTools.getDefaultOutputPath('local') #case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) # If this ^ is used as the destination dir, it will put the remote meshCase dir in the @@ -793,11 +801,9 @@ def deleteMeshcase(self): # local host if self.hostname == 'local': #TODO Check if the meshCase exists - local_prefs = CfdTools.getPreferencesLocation() - local_output_path = FreeCAD.ParamGet(local_prefs).GetString("DefaultOutputPath","") + local_output_path = CfdTools.getDefaultOutputPath('local') print("Local output path:" + local_output_path) - # create the command to do the actual work # TODO This won't work on a Windows or Mac machine Fix it command = "rm -rf " + local_output_path + "/meshCase" @@ -822,12 +828,13 @@ def deleteMeshcase(self): profile_prefs = local_prefs +"/Hosts/" + self.profile_name #remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) # create the command to do the actual work - command = 'ssh -tt ' + self.username + '@' + self.hostname # was -tt - #command += ' << EOT \n' - command = 'cd ' + remote_output_path + '\n' + command = 'ssh -tt ' + self.username + '@' + self.hostname + " " # was -tt + command += ' << EOT \n' + command += 'cd ' + remote_output_path + '\n' command += 'rm -rf meshCase ' + '\n' command += 'exit \n ' command += 'EOT \n' @@ -859,8 +866,13 @@ def copyMeshcaseToHost(self): remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") - local_output_path = FreeCAD.ParamGet(local_prefs).GetString("DefaultOutputPath","") + + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #local_output_path = FreeCAD.ParamGet(local_prefs).GetString("DefaultOutputPath","") + + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + local_output_path = CfdTools.getDefaultOutputPath('local') + #case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) # If this ^ is used as the destination dir, it will put the remote meshCase dir in the diff --git a/CfdOF/Solve/TaskPanelCfdSolverControl.py b/CfdOF/Solve/TaskPanelCfdSolverControl.py index cddbbc50..eafb8f0e 100644 --- a/CfdOF/Solve/TaskPanelCfdSolverControl.py +++ b/CfdOF/Solve/TaskPanelCfdSolverControl.py @@ -156,6 +156,7 @@ def loadProfile(self, profile_name): # set the vars to the local parameters if profile_name == "local": self.hostname = "local" + self.output_path = CfdTools.getDefaultOutputPath('local') # the local code doesn't use these vars, so don't set them # dangerous. """ @@ -180,10 +181,12 @@ def loadProfile(self, profile_name): #self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") #self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") - self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + + #self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = CfdTools.getDefaultOutputPath(self.profile_name) # these are used - self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + #self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") @@ -431,6 +434,7 @@ def runSolverProcess(self, profileName): "if proxy.running_from_macro:\n" + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + + " working_dir = CfdTools.getOutputPath(analysis_object)\n" + " case_name = solver_object.InputCaseName\n" + " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + @@ -462,6 +466,8 @@ def runSolverProcess(self, profileName): "if proxy.running_from_macro:\n" + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + + + # TODO: this might be wrong " working_dir = CfdTools.getOutputPath(analysis_object)\n" + " case_name = solver_object.InputCaseName\n" + " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + @@ -504,9 +510,11 @@ def runSolverProcess(self, profileName): """ # create the command to do the actual work + remote_working_dir = CfdTools.getDefaultOutputPath(self.profile_name) + command = 'ssh -t ' + self.username + '@' + self.hostname # was -tt command += '<< EOT \n' - command += ' cd ' + self.working_dir + '/case \n' + command += ' cd ' + remote_working_dir + '/case \n' command += './Allrun \n' command += 'exit \n ' command += 'EOT \n' @@ -518,6 +526,7 @@ def runSolverProcess(self, profileName): cmd = CfdTools.makeRunCommand(command,None) + # TODO not sure this is correct anymore working_dir = CfdTools.getOutputPath(self.analysis_object) case_name = self.solver_object.InputCaseName solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) @@ -550,6 +559,9 @@ def killSolverProcess(self): # Note: solverFinished will still be called def solverFinished(self, exit_code): + if self.form.cb_notify.isChecked(): + #print("Beeping now") + QApplication.beep() if exit_code == 0: self.consoleMessage("Simulation finished") @@ -563,8 +575,11 @@ def solverFinished(self, exit_code): remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") - local_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #local_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + local_output_path = CfdTools.getDefaultOutputPath('local') # if we are deleting the solver and mesh case on the server # if we delete the mesh case we'll need to remesh before running the solver diff --git a/Gui/TaskPanelCfdMesh.ui b/Gui/TaskPanelCfdMesh.ui index da79d20e..c0eecaef 100644 --- a/Gui/TaskPanelCfdMesh.ui +++ b/Gui/TaskPanelCfdMesh.ui @@ -425,7 +425,7 @@ - + 8 @@ -441,6 +441,20 @@ 8 + + + + Delete mesh case on host + + + + + + + Copy local mesh case to host + + + @@ -469,17 +483,10 @@ - - - - Copy local mesh case to host - - - - - + + - Delete mesh case + Notify when Run mesh is complete diff --git a/Gui/TaskPanelCfdSolverControl.ui b/Gui/TaskPanelCfdSolverControl.ui index 4543ded8..889a357b 100644 --- a/Gui/TaskPanelCfdSolverControl.ui +++ b/Gui/TaskPanelCfdSolverControl.ui @@ -107,6 +107,13 @@ 8 + + + + Write OF case + + + @@ -117,30 +124,30 @@ - - + + true - Run OF case + Edit - - + + true - Edit + Run OF case - - + + - Write OF case + Notify when Run OF case is done From 365cf573c210e63042cf17223da6ef51a693b263 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Sun, 19 Mar 2023 14:07:30 -0600 Subject: [PATCH 13/20] Added add filename to output --- CfdOF/Mesh/TaskPanelCfdMesh.py | 10 ++-------- CfdOF/Solve/TaskPanelCfdSolverControl.py | 4 ---- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 91aa3c19..32f587a7 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -29,11 +29,6 @@ # -This code uses (dangerous) global vars to access things like profile_name, hostname, output dir, etc. # The profile_name, hostname, output dir should be attached to the mesh object and used from it. # -# -Add use filename extension to the output path. For both local and remote processing. -# The value is already saved in prefs for both local and hosts -# You can get it with FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) -# Should be appended to the mesh object ? -# # -the ParaView button gets enabled as soon as the mesh case is written before the results are # available. This should be fixed. See updateUI. # @@ -52,9 +47,8 @@ # # -remote meshing has not been tested in macros. # -# -cfMesh runs only 1 mesh process on the remote machine if 1,0 is selected for processes, threads. If you run 8 threads, for example, it -# doesn't ever finish -# the meshing process stack on the remote host is this. Looks legit, but never finishes. +# -cfMesh runs only 1 mesh process on the remote machine if 1,0 is selected for processes, threads. If you run 8 threads (1,8) for example, it +# doesn't ever finish the meshing process stack on the remote host is this. Looks legit, but never finishes. # # $ ps aux | grep Mesh #me 134162 0.0 0.0 174292 17336 ? Sl 21:58 0:00 mpiexec -np 8 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel diff --git a/CfdOF/Solve/TaskPanelCfdSolverControl.py b/CfdOF/Solve/TaskPanelCfdSolverControl.py index eafb8f0e..d61b4dec 100644 --- a/CfdOF/Solve/TaskPanelCfdSolverControl.py +++ b/CfdOF/Solve/TaskPanelCfdSolverControl.py @@ -40,10 +40,6 @@ # # TODOs (there are some in the code as well) # -# -add filename to the output path. (addFilenameToOutput) For both local and remote useRemoteProcessing. -# It is already saved in prefs, for both local and remote hosts. You can get it with -# FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0) -# # -check on the number of cores that are being asked for and used by OpenFOAM # # -makeRunCommand is using the local OF bash command to build the remote run command. It works but it isn't correct. From 3ea847bea67cf6516053411dd7c08ffe67603724 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Sun, 19 Mar 2023 14:50:35 -0600 Subject: [PATCH 14/20] Turned on add filename to path in Preferences --- CfdOF/CfdPreferencePage.py | 20 ++++++++++++++------ CfdOF/CfdRemotePreferencePage.py | 4 ++-- CfdOF/CfdTools.py | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CfdOF/CfdPreferencePage.py b/CfdOF/CfdPreferencePage.py index 3a8cecbf..1e4dfefb 100644 --- a/CfdOF/CfdPreferencePage.py +++ b/CfdOF/CfdPreferencePage.py @@ -146,6 +146,13 @@ def __init__(self): def __del__(self): self.cleanUp() + # This is a special version that only returns the base output path. + # The version in CfdTools.py returns the full output path, with the filename appended + def getDefaultOutputPath(self): + prefs = CfdTools.getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + return output_path + def cleanUp(self): if self.thread and self.thread.isRunning(): FreeCAD.Console.PrintError("Terminating a pending install task\n") @@ -180,7 +187,8 @@ def loadSettings(self): self.initial_gmsh_path = str(self.gmsh_path) self.form.le_gmsh_path.setText(self.gmsh_path) - self.output_dir = CfdTools.getDefaultOutputPath() + #self.output_dir = CfdTools.getDefaultOutputPath() + self.output_dir = self.getDefaultOutputPath() self.form.le_output_dir.setText(self.output_dir) if FreeCAD.ParamGet(prefs).GetBool("UseDocker", 0): @@ -194,11 +202,11 @@ def loadSettings(self): self.setDownloadURLs() # disable add_filename_to_output for now. - self.form.cb_add_filename_to_output.setEnabled(False) - #if FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0): - # self.form.cb_add_filename_to_output.setChecked(True) - #else: - # self.form.cb_add_filename_to_output.setChecked(False) + # self.form.cb_add_filename_to_output.setEnabled(False) + if FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0): + self.form.cb_add_filename_to_output.setChecked(True) + else: + self.form.cb_add_filename_to_output.setChecked(False) def consoleMessage(self, message="", colour_type=None): message = escape(message) diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py index 6f30cbe0..bb5edbad 100644 --- a/CfdOF/CfdRemotePreferencePage.py +++ b/CfdOF/CfdRemotePreferencePage.py @@ -334,14 +334,14 @@ def enableControls(self,value): self.form.cb_copy_back.setEnabled(value) self.form.cb_delete_remote_results.setEnabled(value) + self.form.cb_add_filename_to_output.setEnabled(value) + # Controls below here are not yet operational regardless of if remote processing # is enabled or not. So they are disabled. Change this as new functionality # is added to the page value = False - # not implmented yet - self.form.cb_add_filename_to_output.setEnabled(value) self.form.pb_about_remote_processing.setEnabled(value) diff --git a/CfdOF/CfdTools.py b/CfdOF/CfdTools.py index 967c7d22..4284a4d1 100644 --- a/CfdOF/CfdTools.py +++ b/CfdOF/CfdTools.py @@ -93,6 +93,7 @@ # Modified by LinuxGuy to handle add_filename_to_path # This is called default because it is used in the mesh and solver objects # but can be over ridden by the user +# WARNING: this routine returns the path with the filename add if enabled in Preferences def getDefaultOutputPath(profile = 'local'): # host is local @@ -104,7 +105,7 @@ def getDefaultOutputPath(profile = 'local'): print("Root output path:" + output_path) add_filename = FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput") - print("AddFilenameToOutput:" + str(add_filename)) + #print("AddFilenameToOutput:" + str(add_filename)) if add_filename: #TODO: might want to warn the user if they haven't saved the filename yet From 5e03bae41a23bd380341e9d5f0f5667d6a6d41e4 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Mon, 20 Mar 2023 10:52:05 -0600 Subject: [PATCH 15/20] Touch ups --- CfdOF/Solve/CfdCaseWriterFoam.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CfdOF/Solve/CfdCaseWriterFoam.py b/CfdOF/Solve/CfdCaseWriterFoam.py index e6d20972..13b441c9 100644 --- a/CfdOF/Solve/CfdCaseWriterFoam.py +++ b/CfdOF/Solve/CfdCaseWriterFoam.py @@ -159,10 +159,13 @@ def writeCase(self, profile_name): # if using a remote host, copy the case folder from the local case dir # to the remote host's directory if profile_name != "local": + profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + profile_name remote_user = ParamGet(profile_prefs).GetString("Username", "") remote_hostname = ParamGet(profile_prefs).GetString("Hostname", "") - remote_output_path = ParamGet(profile_prefs).GetString("OutputPath","") + + #remote_output_path = ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(profile_name) #print("remote_user:" + remote_user) #print("remote_hostname:" + remote_hostname) From ef6e4130d0e07055abfd35517846aa654613ee42 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Mon, 20 Mar 2023 16:09:31 -0600 Subject: [PATCH 16/20] Fixed merge issue --- CfdOF/Mesh/CfdMeshTools.py | 3 +++ CfdOF/Mesh/TaskPanelCfdMesh.py | 20 ++------------------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/CfdOF/Mesh/CfdMeshTools.py b/CfdOF/Mesh/CfdMeshTools.py index 9c652658..9bf987b7 100644 --- a/CfdOF/Mesh/CfdMeshTools.py +++ b/CfdOF/Mesh/CfdMeshTools.py @@ -82,6 +82,9 @@ def writeMesh(self, profile_name): self.progressCallback("Exporting the part surfaces ...") self.writePartFile() self.writeMeshCase(profile_name) + CfdTools.cfdMessage("Wrote mesh case to {}\n".format(self.meshCaseDir)) + if self.progressCallback: + self.progressCallback("Mesh case written successfully") def processExtrusions(self): diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 32f587a7..7a0d420c 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -529,24 +529,12 @@ def checkMeshClicked(self): self.mesh_obj.Proxy.check_mesh_process = CfdConsoleProcess( stdout_hook=self.gotOutputLines, stderr_hook=self.gotErrorLines) FreeCADGui.doCommand("if proxy.running_from_macro:\n" + - " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + - " mesh_process.start(cmd, env_vars=env_vars)\n" + - " mesh_process.waitForFinished()\n" + " proxy.check_mesh_process = CfdConsoleProcess()\n" + " proxy.check_mesh_process.start(cmd, env_vars=env_vars)\n" + " proxy.check_mesh_process.waitForFinished()\n" + "else:\n" + - #" proxy.mesh_process.start(cmd, env_vars=env_vars)"+ " proxy.check_mesh_process.start(cmd, env_vars=env_vars)") - if self.mesh_obj.Proxy.mesh_process.waitForStarted(): - self.form.pb_check_mesh.setEnabled(False) # Prevent user running a second instance - self.form.pb_run_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(False) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) - self.form.pb_load_mesh.setEnabled(False) - if self.mesh_obj.Proxy.check_mesh_process.waitForStarted(): self.consoleMessage("Mesh check started ...") @@ -599,15 +587,9 @@ def runMesh(self): FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") FreeCADGui.doCommand("from CfdOF import CfdTools") FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") - - FreeCADGui.doCommand("from FreeCAD import ParamGet") FreeCADGui.doCommand("cart_mesh = " + "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") - - - FreeCADGui.doCommand("cart_mesh = " - " CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") @@ -632,6 +614,7 @@ def runMesh(self): "else:\n" + " proxy.mesh_process.start(cmd, env_vars=env_vars)") + # run on remote host else: #self.runRemoteMesh() #For testing the non proxy function above @@ -679,6 +662,7 @@ def runMesh(self): "else:\n" + " proxy.mesh_process.start(runCommand)") + if self.mesh_obj.Proxy.mesh_process.waitForStarted(): # enable/disable the correct buttons self.form.pb_stop_mesh.setEnabled(True) From f64092b54eb2ce5149eb3e654401c340ee3df60b Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Wed, 22 Mar 2023 14:59:19 -0600 Subject: [PATCH 17/20] modified: Gui/Icons/preferences-cfdof.svg --- Gui/Icons/preferences-cfdof.svg | 83 +++++++++------------------------ 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/Gui/Icons/preferences-cfdof.svg b/Gui/Icons/preferences-cfdof.svg index 1f471823..b0298dda 100644 --- a/Gui/Icons/preferences-cfdof.svg +++ b/Gui/Icons/preferences-cfdof.svg @@ -1,62 +1,23 @@ - - - - - - image/svg+xml - - - - - - - - + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + From 0d73951e227a74ee1dc5e31fc9521b928b739229 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 23 Mar 2023 01:06:06 -0600 Subject: [PATCH 18/20] modified: CfdOF/CfdRemotePreferencePage.py modified: CfdOF/CfdTools.py modified: CfdOF/Solve/CfdCaseWriterFoam.py --- CfdOF/CfdRemotePreferencePage.py | 2 ++ CfdOF/CfdTools.py | 3 +++ CfdOF/Solve/CfdCaseWriterFoam.py | 6 +++++- TestCfdOF.py | 0 4 files changed, 10 insertions(+), 1 deletion(-) mode change 100644 => 100755 TestCfdOF.py diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py index bb5edbad..0a584d34 100644 --- a/CfdOF/CfdRemotePreferencePage.py +++ b/CfdOF/CfdRemotePreferencePage.py @@ -1230,6 +1230,8 @@ def pickParaviewFile(self): #********************************************************************************************** # TODO: Update to use profiles + # TODO: Check if cfmesh is already installed before installing in case the user + # accidentally pushes the button def remoteDownloadInstallCfMesh(self): #TODO: this routine doesn't clean up after itself if it fails diff --git a/CfdOF/CfdTools.py b/CfdOF/CfdTools.py index 4284a4d1..8f498796 100644 --- a/CfdOF/CfdTools.py +++ b/CfdOF/CfdTools.py @@ -156,6 +156,8 @@ def getDefaultRemoteOutputPath(): return output_path +# This used to be called by CfdCaseWriterFoam, but isn't anymore. +# Not sure what uses it def getOutputPath(analysis): if analysis and 'OutputPath' in analysis.PropertiesList: output_path = analysis.OutputPath @@ -171,6 +173,7 @@ def getOutputPath(analysis): output_path = os.path.normpath(output_path) return output_path + # TODO Is this used anymore ? def getRemoteOutputPath(analysis): if analysis and 'RemoteOutputPath' in analysis.PropertiesList: diff --git a/CfdOF/Solve/CfdCaseWriterFoam.py b/CfdOF/Solve/CfdCaseWriterFoam.py index 13b441c9..2c4eae27 100644 --- a/CfdOF/Solve/CfdCaseWriterFoam.py +++ b/CfdOF/Solve/CfdCaseWriterFoam.py @@ -52,7 +52,11 @@ def __init__(self, analysis_obj): self.zone_objs = CfdTools.getZoneObjects(analysis_obj) self.dynamic_mesh_refinement_obj = CfdTools.getDynamicMeshAdaptation(self.mesh_obj) self.mesh_generated = False - self.working_dir = CfdTools.getOutputPath(self.analysis_obj) + + # LG123 changed this + # self.working_dir = CfdTools.getOutputPath(self.analysis_obj) + self.working_dir = CfdTools.getDefaultOutputPath('local') + self.progressCallback = None self.settings = None diff --git a/TestCfdOF.py b/TestCfdOF.py old mode 100644 new mode 100755 From c9fe7df835503735c4433118ad0595de89f36a60 Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Thu, 23 Mar 2023 22:24:58 -0600 Subject: [PATCH 19/20] modified: CfdOF/CfdRemotePreferencePage.py modified: CfdOF/Mesh/TaskPanelCfdMesh.py modified: CfdOF/Solve/CfdCaseWriterFoam.py modified: CfdOF/Solve/TaskPanelCfdSolverControl.py --- CfdOF/CfdRemotePreferencePage.py | 7 ++++++- CfdOF/Mesh/TaskPanelCfdMesh.py | 10 ++++++++-- CfdOF/Solve/CfdCaseWriterFoam.py | 6 +++--- CfdOF/Solve/TaskPanelCfdSolverControl.py | 24 +++++++----------------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py index 0a584d34..8b2af3f4 100644 --- a/CfdOF/CfdRemotePreferencePage.py +++ b/CfdOF/CfdRemotePreferencePage.py @@ -1237,10 +1237,15 @@ def remoteDownloadInstallCfMesh(self): #TODO: this routine doesn't clean up after itself if it fails # Nor does it check for a full or partial build # If the user reruns the build after fully or partially building previously - # this routine will fail. + # this routine will fail because the cfmesh directory is already present. # TODO this routine assumes the output dir exists. Will fail if it doesn't. Fix this. + # TODO: if the remote host is missingzlib-dev, this routine fails silently and then the user will get + # an error /usr/bin/ld: checkSurfaceMesh.C:(.text.startup+0xabc): undefined reference to `Foam::triSurf::~triSurf()' + # when the solver is run See here: https://forum.freecad.org/viewtopic.php?style=10&t=60010 + # This appears to be a problem with the local install process as well. + # Get the username and hostname for the remote host remote_user = self.username remote_hostname = self.hostname diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 7a0d420c..fd191ea7 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -237,8 +237,11 @@ def loadProfile(self, profile_name): hostPrefs = self.host_prefs_location self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + + # This is loading defaults. The user may change this number self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") @@ -250,8 +253,11 @@ def loadProfile(self, profile_name): self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") #now set the control values - self.mesh_obj.NumberOfProcesses = self.mesh_processes - self.mesh_obj.NumberOfThreads = self.mesh_threads + #read these from the parameters in case they've been edited + self.mesh_obj.NumberOfProcesses = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_obj.NumberOfThreads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + + # disable if using a remote host self.form.pb_edit_mesh.setEnabled(False) diff --git a/CfdOF/Solve/CfdCaseWriterFoam.py b/CfdOF/Solve/CfdCaseWriterFoam.py index 2c4eae27..b70051d8 100644 --- a/CfdOF/Solve/CfdCaseWriterFoam.py +++ b/CfdOF/Solve/CfdCaseWriterFoam.py @@ -53,9 +53,9 @@ def __init__(self, analysis_obj): self.dynamic_mesh_refinement_obj = CfdTools.getDynamicMeshAdaptation(self.mesh_obj) self.mesh_generated = False - # LG123 changed this - # self.working_dir = CfdTools.getOutputPath(self.analysis_obj) - self.working_dir = CfdTools.getDefaultOutputPath('local') + # LG123 changed this for testing + self.working_dir = CfdTools.getOutputPath(self.analysis_obj) + #self.working_dir = CfdTools.getDefaultOutputPath('local') self.progressCallback = None diff --git a/CfdOF/Solve/TaskPanelCfdSolverControl.py b/CfdOF/Solve/TaskPanelCfdSolverControl.py index d61b4dec..54760474 100644 --- a/CfdOF/Solve/TaskPanelCfdSolverControl.py +++ b/CfdOF/Solve/TaskPanelCfdSolverControl.py @@ -40,7 +40,7 @@ # # TODOs (there are some in the code as well) # -# -check on the number of cores that are being asked for and used by OpenFOAM +# - the solver # # -makeRunCommand is using the local OF bash command to build the remote run command. It works but it isn't correct. # The remote run is not calling the source command to set up OF usage. It is relying on the bash shell on the remote host @@ -54,7 +54,9 @@ # # -remote solving has not been tested in macros # -#- presently does not set the number of threads that the solver uses properly. Must be done manually. +#- presently does not set the number of threads that the solver uses properly. Must be done manually. Not sure where +# the default is set. It runs with the number of cores in the editor, but that should be set to the preferences value +# when the host is changed. It doesn't. The field name is "ParallelCores" as far as I can tell. class TaskPanelCfdSolverControl: def __init__(self, solver_runner_obj): @@ -172,6 +174,8 @@ def loadProfile(self, profile_name): hostPrefs = self.host_prefs_location self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + + #self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") #self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") #self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") @@ -194,21 +198,6 @@ def loadProfile(self, profile_name): #self.mesh_obj.NumberOfProcesses = self.mesh_processes #self.mesh_obj.NumberOfThreads = self.mesh_threads - #TODO: fix these, if we need to. - # Leaving these in in case we add controls to set these someday - #self.form.le_mesh_processes.setText(str(self.mesh_processes)) - #self.form.le_mesh_threads.setText(str(self.mesh_threads)) - - #self.form.le_hostname.setText(self.hostname) - #self.form.le_username.setText(self.username) - - #self.form.le_foam_processes.setText(str(self.foam_processes)) - #self.form.le_foam_threads.setText(str(self.foam_threads)) - #self.form.le_foam_dir.setText(self.foam_dir) - #self.form.le_output_path.setText(self.output_path) - #self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) - - # this gets called when the user changes the profile def profileChanged(self): print("The profile was changed") @@ -426,6 +415,7 @@ def runSolverProcess(self, profileName): # if running on local host if self.profile_name == "local": # This must be kept in one doCommand because of the if statement + # TODO: check that the doCommand code is the same as the inline code FreeCADGui.doCommand( "if proxy.running_from_macro:\n" + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + From 40b10a085f66bb86b03f9daca0f5699fa291553f Mon Sep 17 00:00:00 2001 From: linuxguy123 Date: Sat, 25 Mar 2023 22:15:26 -0600 Subject: [PATCH 20/20] modified: CfdOF/Mesh/TaskPanelCfdMesh.py --- CfdOF/Mesh/TaskPanelCfdMesh.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index fd191ea7..9717b224 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -566,6 +566,10 @@ def editMesh(self): CfdTools.openFileManager(case_path) def runMesh(self): + + #disable the run button so a second instance isn't started + self.form.pb_run_mesh.setEnabled(False) + if CfdTools.getFoamRuntime() == "PosixDocker": CfdTools.startDocker()