From 54dc46290a0db6a336b7e1694b0ba93a6d0e75cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Mon, 8 Apr 2024 12:51:09 +0200 Subject: [PATCH] feat: Implemented fit of experim. medium data. It's only good for dispersive media; specifically, a text file with three floats per line: 'wl n k'. A custom script was used to convert Maxim's data. It's very fast, and has a ton of options. Only the most important are exposed in the node for now. A bug in MPL plotting aspect ratio declaration on MPL axis object was fixed by manually running `set_aspect('auto')` after the fact. It shouldn't do anything, and it doesn't, other than fix the bug :) Also brought the plotting function of the viewer to parity with the 3D preview, so the "Auto Plot" button works as expected. --- TODO.md | 2 +- .../managed_objs/managed_bl_image.py | 5 + .../file_importers/tidy_3d_file_importer.py | 156 ++++++++++++++++-- .../maxwell_sim_nodes/nodes/outputs/viewer.py | 16 +- 4 files changed, 158 insertions(+), 21 deletions(-) diff --git a/TODO.md b/TODO.md index e5de3bc..2449f17 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ # Acute Tasks -- Implement Material Import for Maxim Data +- [x] Implement Material Import for Maxim Data - Move preview GN trees to the asset library. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py index bc7492a..da69b3a 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py @@ -6,8 +6,11 @@ import matplotlib.axis as mpl_ax import numpy as np import typing_extensions as typx +from ....utils import logger from .. import contracts as ct +log = logger.get(__name__) + AREA_TYPE = 'IMAGE_EDITOR' SPACE_TYPE = 'IMAGE_EDITOR' @@ -163,6 +166,7 @@ class ManagedBLImage(ct.schemas.ManagedObj): # Compute Plot Dimensions aspect_ratio = _width_inches / _height_inches + log.debug('Create MPL Axes (aspect=%d, width=%d, height=%d)', aspect_ratio, _width_inches, _height_inches) # Create MPL Figure, Axes, and Compute Figure Geometry fig, ax = plt.subplots( figsize=[_width_inches, _height_inches], @@ -171,6 +175,7 @@ class ManagedBLImage(ct.schemas.ManagedObj): ax.set_aspect(aspect_ratio) cmp_width_px, cmp_height_px = fig.canvas.get_width_height() ## Use computed pixel w/h to preempt off-by-one size errors. + ax.set_aspect('auto') ## Workaround aspect-ratio bugs # Plot w/User Parameter func_plotter(ax) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py index c5b40d4..4a9e6b7 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py @@ -3,21 +3,39 @@ from pathlib import Path import bpy import tidy3d as td +import tidy3d.plugins.dispersion as td_dispersion from ......utils import logger from .... import contracts as ct -from .... import sockets +from .... import managed_objs, sockets from ... import base, events log = logger.get(__name__) -TD_FILE_EXTS = { - '.hdf5.gz', - '.hdf5', - '.json', - '.yaml', +VALID_FILE_EXTS = { + 'SIMULATION_DATA': { + '.hdf5.gz', + '.hdf5', + }, + 'SIMULATION': { + '.hdf5.gz', + '.hdf5', + '.json', + '.yaml', + }, + 'MEDIUM': { + '.hdf5.gz', + '.hdf5', + '.json', + '.yaml', + }, + 'EXPERIM_DISP_MEDIUM': { + '.txt', + }, } +CACHE = {} + #################### # - Node @@ -29,25 +47,69 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode): input_sockets: typ.ClassVar = { 'File Path': sockets.FilePathSocketDef(), } + managed_obj_defs: typ.ClassVar = { + 'plot': ct.schemas.ManagedObjDef( + mk=lambda name: managed_objs.ManagedBLImage(name), + name_prefix='', + ), + } + #################### + # - Properties + #################### tidy3d_type: bpy.props.EnumProperty( name='Tidy3D Type', description='Type of Tidy3D object to load', items=[ ( 'SIMULATION_DATA', - 'Simulation Data', + 'Sim Data', 'Data from Completed Tidy3D Simulation', ), - ('SIMULATION', 'Simulation', 'Tidy3D Simulation'), + ('SIMULATION', 'Sim', 'Tidy3D Simulation'), ('MEDIUM', 'Medium', 'A Tidy3D Medium'), + ( + 'EXPERIM_DISP_MEDIUM', + 'Experim Disp Medium', + 'A pole-residue fit of experimental dispersive medium data, described by a .txt file specifying wl, n, k', + ), ], default='SIMULATION_DATA', update=lambda self, context: self.sync_prop('tidy3d_type', context), ) + disp_fit__min_poles: bpy.props.IntProperty( + name='min Poles', + description='Min. # poles to fit to the experimental dispersive medium data', + default=1, + ) + disp_fit__max_poles: bpy.props.IntProperty( + name='max Poles', + description='Max. # poles to fit to the experimental dispersive medium data', + default=5, + ) + ## TODO: Bool of whether to fit eps_inf, with conditional choice of eps_inf as socket + disp_fit__tolerance_rms: bpy.props.FloatProperty( + name='Max RMS', + description='The RMS error threshold, below which the fit should be considered converged', + default=0.001, + precision=5, + ) + ## TODO: "AdvanceFastFitterParam" options incl. loss_bounds, weights, show_progress, show_unweighted_rms, relaxed, smooth, logspacing, numiters, passivity_num_iters, and slsqp_constraint_scale + + def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout): + col.prop(self, 'tidy3d_type', text='') + if self.tidy3d_type == 'EXPERIM_DISP_MEDIUM': + row = col.row(align=True) + row.alignment = 'CENTER' + row.label(text='Pole-Residue Fit') + + col.prop(self, 'disp_fit__min_poles') + col.prop(self, 'disp_fit__max_poles') + col.prop(self, 'disp_fit__tolerance_rms') + #################### - # - Event Methods: Compute Output Data + # - Event Methods: Output Data #################### def _compute_sim_data_for( self, output_socket_name: str, file_path: Path @@ -79,6 +141,39 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode): def compute_medium(self, input_sockets: dict) -> td.Medium: return self._compute_sim_data_for('Medium', input_sockets['File Path']) + #################### + # - Event Methods: Output Data | Dispersive Media + #################### + @events.computes_output_socket( + 'Experim Disp Medium', + input_sockets={'File Path'}, + ) + def compute_experim_disp_medium(self, input_sockets: dict) -> td.Medium: + if CACHE.get(self.bl_label) is not None: + log.debug('Reusing Cached Dispersive Medium') + return CACHE[self.bl_label]['model'] + + log.info('Loading Experimental Data') + dispersion_fitter = td_dispersion.FastDispersionFitter.from_file( + str(input_sockets['File Path']) + ) + + log.info('Computing Fast Dispersive Fit of Experimental Data...') + pole_residue_medium, rms_error = dispersion_fitter.fit( + min_num_poles=self.disp_fit__min_poles, + max_num_poles=self.disp_fit__max_poles, + tolerance_rms=self.disp_fit__tolerance_rms, + ) + log.info('Fit Succeeded w/RMS "%s"!', f'{rms_error:.5f}') + + # Populate Cache + CACHE[self.bl_label] = {} + CACHE[self.bl_label]['model'] = pole_residue_medium + CACHE[self.bl_label]['fitter'] = dispersion_fitter + CACHE[self.bl_label]['rms_error'] = rms_error + + return pole_residue_medium + #################### # - Event Methods: Setup Output Socket #################### @@ -89,8 +184,14 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode): props={'tidy3d_type'}, ) def on_file_changed(self, input_sockets: dict, props: dict): + if CACHE.get(self.bl_label) is not None: + del CACHE[self.bl_label] + file_ext = ''.join(input_sockets['File Path'].suffixes) - if not (input_sockets['File Path'].is_file() and file_ext in TD_FILE_EXTS): + if not ( + input_sockets['File Path'].is_file() + and file_ext in VALID_FILE_EXTS[props['tidy3d_type']] + ): self.loose_output_sockets = {} else: self.loose_output_sockets = { @@ -99,8 +200,43 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode): }, 'SIMULATION': {'Sim': sockets.MaxwellFDTDSimDataSocketDef()}, 'MEDIUM': {'Medium': sockets.MaxwellMediumSocketDef()}, + 'EXPERIM_DISP_MEDIUM': { + 'Experim Disp Medium': sockets.MaxwellMediumSocketDef() + }, }[props['tidy3d_type']] + #################### + # - Event Methods: Plot + #################### + @events.on_show_plot( + managed_objs={'plot'}, + props={'tidy3d_type'}, + ) + def on_show_plot( + self, + props: dict, + managed_objs: dict, + ): + """When the filetype is 'Experimental Dispersive Medium', plot the computed model against the input data.""" + if props['tidy3d_type'] == 'EXPERIM_DISP_MEDIUM': + # Populate Cache + if CACHE.get(self.bl_label) is None: + model_medium = self.compute_experim_disp_medium() + disp_fitter = CACHE[self.bl_label]['fitter'] + else: + model_medium = CACHE[self.bl_label]['model'] + disp_fitter = CACHE[self.bl_label]['fitter'] + + # Plot + log.debug(disp_fitter) + managed_objs['plot'].mpl_plot_to_image( + lambda ax: disp_fitter.plot( + medium=model_medium, + ax=ax, + ), + bl_select=True, + ) + #################### # - Blender Registration diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py index 65897ff..249f7af 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -23,7 +23,6 @@ class ConsoleViewOperator(bpy.types.Operator): def execute(self, context): node = context.node - print('Executing Operator') node.print_data_to_console() return {'FINISHED'} @@ -39,7 +38,7 @@ class RefreshPlotViewOperator(bpy.types.Operator): def execute(self, context): node = context.node - node.trigger_action('value_changed', 'Data') + node.on_changed_plot_preview() return {'FINISHED'} @@ -67,7 +66,7 @@ class ViewerNode(base.MaxwellSimNode): auto_3d_preview: bpy.props.BoolProperty( name='Auto 3D Preview', description="Whether to auto-preview anything 3D, that's plugged into the viewer node", - default=False, + default=True, update=lambda self, context: self.sync_prop('auto_3d_preview', context), ) @@ -126,18 +125,14 @@ class ViewerNode(base.MaxwellSimNode): # - Event Methods #################### @events.on_value_changed( - socket_name='Data', + prop_name='auto_plot', props={'auto_plot'}, ) - def on_changed_2d_data(self, props): - # Show Plot - ## Don't have to un-show other plots. + def on_changed_plot_preview(self, props): if self.inputs['Data'].is_linked and props['auto_plot']: + log.info('Enabling 2D Plot from "%s"', self.name) self.trigger_action('show_plot') - #################### - # - Event Methods: 3D Preview - #################### @events.on_value_changed( prop_name='auto_3d_preview', props={'auto_3d_preview'}, @@ -159,6 +154,7 @@ class ViewerNode(base.MaxwellSimNode): # Just Linked / Just Unlinked: Preview/Unpreview if self.inputs['Data'].is_linked ^ self.cache__data_socket_linked: self.on_changed_3d_preview() + self.on_changed_plot_preview() self.cache__data_socket_linked = self.inputs['Data'].is_linked