Skip to content

Instantly share code, notes, and snippets.

@sjgallagher2
Created September 3, 2025 18:17
Show Gist options
  • Select an option

  • Save sjgallagher2/20e0aeae38b5a20870460e7cd29cd2ea to your computer and use it in GitHub Desktop.

Select an option

Save sjgallagher2/20e0aeae38b5a20870460e7cd29cd2ea to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Sep 3 12:41:04 2025
@author: sam
"""
import sys
import os
import time
import re
import gmsh
os.environ["QT_API"] = "pyside6"
from qtpy import QtWidgets,QtCore
from pyvistaqt import QtInteractor, MainWindow
from pathlib import Path
from emerge._emerge.plot.pyvista import PVBackgroundDisplay
class EmergeViewerMainWindow(MainWindow):
def __init__(self, parent=None, show=True):
QtWidgets.QMainWindow.__init__(self, parent)
self.fname = ''
self.fs_watcher = QtCore.QFileSystemWatcher([self.fname])
self._cached_geom_str = ''
# create the frame
self.frame = QtWidgets.QFrame()
vlayout = QtWidgets.QVBoxLayout()
# add the pyvista interactor object
self.plotter = QtInteractor(self.frame)
vlayout.addWidget(self.plotter.interactor)
self.signal_close.connect(self.plotter.close)
self.frame.setLayout(vlayout)
self.setCentralWidget(self.frame)
# simple menu to demo functions
main_menu = self.menuBar()
file_menu = main_menu.addMenu('File')
open_button = QtWidgets.QAction('Open', self)
open_button.setShortcut('Ctrl+O')
open_button.triggered.connect(self.file_open_dlg)
file_menu.addAction(open_button)
exit_button = QtWidgets.QAction('Exit', self)
exit_button.setShortcut('Ctrl+Q')
exit_button.triggered.connect(self.close)
file_menu.addAction(exit_button)
mesh_menu = main_menu.addMenu('Geometry')
self.recompile_action = QtWidgets.QAction('Recompile', self)
self.recompile_action.triggered.connect(self.recompile)
mesh_menu.addAction(self.recompile_action)
self.fs_watcher.fileChanged.connect(self.recompile)
if show:
self.show()
self.update_geom_lines()
self.execute_file()
self.cam_pos_saved = self.plotter.camera.position
self.cam_foc_saved = self.plotter.camera.focal_point
self.cam_up_saved = self.plotter.camera.up
self.last_update_time = time.time()
def file_open_dlg(self):
fname_sel = QtWidgets.QFileDialog.getOpenFileName(self, "Open Image", str(Path.home()), "Python Files (*.py)")
fname_sel = fname_sel[0]
if fname_sel != '':
self.fname = fname_sel
self.fs_watcher.removePaths(self.fs_watcher.files())
self.fs_watcher.addPath(self.fname)
self.recompile()
@staticmethod
def _get_model_name(geom_lines):
if geom_lines is not None:
# Regex for finding an identifier
pattern = re.compile(
r"""^
([A-Za-z_][A-Za-z0-9_\.]*) # valid Python identifier
\s*=\s* # equals sign with optional spaces
[A-Za-z_][A-Za-z0-9_]*\.Simulation(.*) # valid identifier (package name) then Simulation(...)
$""",
re.VERBOSE,)
model_varname = ''
for i,line in enumerate(geom_lines):
if line != '\n':
# print(f'{line[:-1]} -> {bool(pattern.match(line))}')
match = pattern.match(line)
if bool(match):
# This is a variable assignment, check if it's for the Simulation object
model_varname = match.group(1)
if model_varname != '':
return model_varname,i # Only first model is returned
else:
# Might be a with...as statement. Try to detect it.
pattern = re.compile(
r"""
with\s[A-Za-z_][A-Za-z0-9_]*\.Simulation(.*)
\sas\s
([A-Za-z_][A-Za-z0-9_\.]*).*$""",
re.VERBOSE,)
for i,line in enumerate(geom_lines):
if line != '\n':
# print(f'{line[:-1]} -> {bool(pattern.match(line))}')
match = pattern.match(line.strip())
if bool(match):
# This is a variable assignment, check if it's for the Simulation object
model_varname = match.group(2)
break
if model_varname != '':
return model_varname,i # Only first model is returned
else:
raise ValueError("No suitable assignment of Simulation object found.")
@staticmethod
def _get_geometry_lines(lines,model_name):
indent = ''
# Look for matching lines
pattern = re.compile(f'{model_name}.commit_geometry(.*).*')
end_idx = -1
for line_no in range(len(lines)):
if bool(pattern.match(lines[line_no].strip())):
end_idx = line_no + 1 # Include this line
indent = lines[line_no][ :lines[line_no].find(model_name) ]
if end_idx == -1:
# Might be implicitly calling commit_geometry() inside generate_mesh()
pattern = re.compile(f'{model_name}.generate_mesh().*')
end_idx = -1
for line_no in range(len(lines)):
if bool(pattern.match(lines[line_no].strip())):
end_idx = line_no # Exclude this line
indent = lines[line_no][ :lines[line_no].find(model_name) ]
if end_idx == -1:
raise ValueError(f"commit_geometry() command not found with model `{model_name}`.")
geom_lines = lines[:end_idx]
geom_lines = [gl for gl in geom_lines if gl[0] != '#']
# geom_lines = EmergeViewerMainWindow._join_continued_lines(geom_lines)
return geom_lines,indent # Return first match to commit_geometry()
def recompile(self):
# Recompile, with debounce for editors that delete and resave a file
if self.fname == '':
return
current_time = time.time()
if current_time - self.last_update_time > 1:
did_update = self.update_geom_lines()
if did_update:
# Save camera state
self.cam_pos_saved = self.plotter.camera.position
self.cam_foc_saved = self.plotter.camera.focal_point
self.cam_up_saved = self.plotter.camera.up
# Clear
self.plotter.clear_actors()
# Run geometry scripts
self.execute_file()
# Restore camera state
self.plotter.camera.position = self.cam_pos_saved
self.plotter.camera.focal_point = self.cam_foc_saved
self.plotter.camera.up = self.cam_up_saved
self.last_update_time = current_time
def update_geom_lines(self) -> bool:
"""Read file and update cached geometry text, return True if updated, False if no change"""
if self.fname == '':
return False
with open(self.fname, 'r') as f:
lines = f.readlines()
model_name,model_def_line = EmergeViewerMainWindow._get_model_name(lines)
if model_name == '':
raise ValueError("Could not find suitable model name definition.")
geom_lines,indent = EmergeViewerMainWindow._get_geometry_lines(lines,model_name)
# Post-process geometry lines
# Add view update
geom_str = ''.join(geom_lines)
# print(geom_str)
view_update_s = f"""{indent}{model_name}.display = PVBackgroundDisplay({model_name}.mesh, self.plotter)
{indent}try:
{indent} gmsh.model.occ.synchronize() # this is global
{indent} gmsh.model.mesh.generate(3)
{indent} {model_name}.mesh.update()
{indent} {model_name}.mesh.exterior_face_tags = {model_name}.mesher.domain_boundary_face_tags
{indent} gmsh.model.occ.synchronize()
{indent} {model_name}._set_mesh({model_name}.mesh)
{indent}except Exception:
{indent} print("GMSH Mesh error. Using generate_mesh()...")
{indent} {model_name}.generate_mesh()
{indent} print("Success.")
{indent}for geo in {model_name}.data.sim['geometries']:
{indent} # display is a PVBackgroundDisplay object
{indent} # This is initialized with the Mesh3D object model.mesh
{indent} {model_name}.display.add_object(geo, opacity=0.5)
"""
geom_str += view_update_s
# print(geom_str)
if geom_str != self._cached_geom_str:
self._cached_geom_str = geom_str
return True
else:
return False
def execute_file(self):
exec(self._cached_geom_str)
# pass
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = EmergeViewerMainWindow()
sys.exit(app.exec_())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment