├── .gitignore ├── IntuitionRF.blend ├── IntuitionRF.blend1 ├── LICENSE ├── README.md ├── __init__.py ├── images ├── check_for_updates.png ├── demo.png ├── panels.png ├── preferences.png ├── reload_scripts.png ├── syspath.png ├── windows_2.png ├── windows_3.png ├── windows_4.png ├── windows_5.png ├── windows_6.png ├── windows_7.png ├── windows_8.png └── windows_9.png ├── make_release.sh ├── nodes └── geometry_source.py ├── operators ├── convert.py └── meshing.py └── panels ├── objects.py └── scene.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | **/__pycache__ 3 | -------------------------------------------------------------------------------- /IntuitionRF.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/IntuitionRF.blend -------------------------------------------------------------------------------- /IntuitionRF.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/IntuitionRF.blend1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IntuitionRF 2 | IntuitionRF is an OpenEMS wrapper plugin for blender aiming to make dealing with RF simulation more intuitive. 3 | 4 | OpenEMS project page : https://www.openems.de/ 5 | 6 | ## Example 7 | 8 | ![patch](images/demo.png) 9 | 10 | ## Contents 11 | - [IntuitionRF](#intuitionrf) 12 | - [Example](#example) 13 | - [Contents](#contents) 14 | - [Install](#install) 15 | - [1. Install OpenEMS](#1-install-openems) 16 | - [2. Install Additionnal python deps](#2-install-additionnal-python-deps) 17 | - [3. Install this plugin](#3-install-this-plugin) 18 | - [4. Enable the addon](#4-enable-the-addon) 19 | - [5. Reload plugins](#5-reload-plugins) 20 | - [6. Properties](#6-properties) 21 | - [7. Checking for updates](#7-checking-for-updates) 22 | - [Install procedure on Windows with pre-built OpenEMS distribution](#install-procedure-on-windows-with-pre-built-openems-distribution) 23 | - [OpenEMS](#openems) 24 | - [install the addon](#install-the-addon) 25 | - [configure the addon](#configure-the-addon) 26 | - [check install](#check-install) 27 | 28 | 29 | 30 | https://www.youtube.com/watch?v=oCE_hrCGen4 31 | [![IntuitionRF - Youtube](https://img.youtube.com/vi/oCE_hrCGen4/0.jpg)](https://www.youtube.com/watch?v=oCE_hrCGen4) 32 | 33 | 34 | ## Install 35 | ### 1. Install OpenEMS 36 | IntuitionRF does not provide a OpenEMS distribution, you have to install a version yourself. 37 | 38 | [Install Instructions for OpenEMS](https://docs.openems.de/install/index.html) 39 | 40 | Requirements: 41 | - OpenEMS must be built with python interface enabled 42 | - OpenEMS must be built against the same python version blender is using 43 | 44 | ### 2. Install Additionnal python deps 45 | Additional Python dependencies: 46 | ```bash 47 | pip install vtk 48 | pip install matplotlib 49 | pip install numpy 50 | pip install h5py 51 | ``` 52 | Once you have the python examples from OpenEMS running, 53 | 54 | ### 3. Install this plugin 55 | Download this repo as ```.zip``` file then install as a regular addon 56 | 57 | ### 4. Enable the addon 58 | 1. Enable the addon 59 | 60 | ![enable the addon](images/preferences.png) 61 | 62 | 2. put your OpenEMS's python version syspath into this. 63 | 64 | - If you compiled OpenEMS against your system's python version, you can use the 'detect systen' to get the syspath automatically 65 | 66 | - If you compiled OpenEMS against a virtualized environment (conda, venv, ...) then run the following in the python interpreter : 67 | ```python 68 | import sys 69 | print(sys.path) 70 | ``` 71 | 72 | then copy the output to the addon's configuration syspath 73 | 74 | ### 5. Reload plugins 75 | 76 | Use blender's 'reload scripts' (F3->reload scripts). The plugin should now be ready. If not, try restarting blender 77 | 78 | ![syspath](images/syspath.png) 79 | ![reload](images/reload_scripts.png) 80 | 81 | ### 6. Properties 82 | You should now see new IntuitionRF properties panels under the 'object' and 'scene' categories. 83 | ![panels](images/panels.png) 84 | 85 | ### 7. Checking for updates 86 | This plugin might be updated with bug fixes or new features. If you find a bug or have an issue with the plugin make sure to run the latest version. 87 | 88 | A feature lets you check the plugin's latest release using Github's API. This is not automatic. This will not install anything, just tell you if you're up to date. 89 | 90 | **I do not collect any telemetry/data from the plugin's usage. Feel free to manually check for updates if you do not feel like using Github's API** 91 | 92 | ![check updates](images/check_for_updates.png) 93 | 94 | ### Install procedure on Windows with pre-built OpenEMS distribution 95 | #### OpenEMS 96 | If you choose to use the binary distribution for OpenEMS on windows here are the steps you need to follow. 97 | 98 | - make sure you download the OpenEMS binaries with python bindings 99 | - install the python libraries as per OpenEMS's install instructions 100 | 101 | ![install deps](images/windows_2.png) 102 | 103 | ```bash 104 | pip install numpy matplotlib vtk h5py 105 | cd LETTER:/your/OpenEMS/extract/dir/python 106 | pip install openEMS-0.0.33-cp311-cp311-win64_amd64.whl 107 | pip install CSXCAD-0.6.2-cp311-cp311-win64_amd64.whl 108 | ``` 109 | 110 | #### install the addon 111 | install then enable the addon 112 | ![addon install](images/windows_4.png) 113 | 114 | #### configure the addon 115 | You need to configure the system python's syspath and openEMS install dir 116 | ![addon install](images/windows_5.png) 117 | 118 | 1. syspath 119 | Open python in a terminal, then 120 | ```python 121 | import sys 122 | sys.path 123 | ``` 124 | 125 | 126 | ![addon install](images/windows_3.png) 127 | 128 | copy the path, paste it into the syspath 129 | 130 | 2. OpenEMS DLL directory 131 | Open the file dialog, and select the directory with OpenEMS's DLL files 132 | 133 | ![addon install](images/windows_6.png) 134 | 135 | Addon is now configured 136 | 137 | ![addon install](images/windows_7.png) 138 | 139 | restart blender 140 | 141 | #### check install 142 | Open blender CLI output, this is where you can see what's going on in openEMS 143 | 144 | ![addon install](images/windows_8.png) 145 | 146 | You should have the new properties panels 147 | 148 | ![addon install](images/windows_9.png) 149 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "IntuitionRF an OpenEMS wrapper for blender", 3 | "blender": (2, 80, 0), 4 | "category": "Object", 5 | "version": (0,3,0), 6 | "description": "A plugin for setting up OpenEMS simulations in Blender" 7 | } 8 | # bl_info = {"name": "My Test Addon", "category": "Object"} 9 | #bl_info = { 10 | # "name": "My Addon", 11 | # "blender": (2, 80, 0), 12 | # "category": "Object", 13 | # "version": (1, 0, 0), 14 | # "location": "View3D > Add > Mesh > My Addon", 15 | # "description": "An example add-on", 16 | # "warning": "", 17 | # "wiki_url": "", 18 | # "tracker_url": "", 19 | # "support": "COMMUNITY", 20 | #} 21 | 22 | import sys 23 | if __name__ == "__main__": 24 | # for release building 25 | if sys.argv[1] == "--version": 26 | v = bl_info["version"] 27 | print(f"{v[0]}.{v[1]}.{v[2]}") 28 | sys.exit(1) 29 | 30 | # this is a horrible workaround to get multiprocessing to work on windows systems 31 | # by default forking with multiprocessing.Process (or multiprocessing.Pool) would cause the 32 | # whole init sequence to trigger for each process except import bpy would fail because it is 33 | # only accessible from a blender context, which is not needed under forked context because we are 34 | # only processing files (vtr to vdb). pyopenvdb, shipped with blender, is still required for this 35 | # to work without requiring yet another external dependency 36 | # TODO find a way to ignore import failures only in a forked context 37 | # or find a proper way to import in forked context on windows 38 | try: 39 | import bpy 40 | import bmesh 41 | import mathutils 42 | from mathutils import geometry 43 | 44 | from bpy.types import Operator, AddonPreferences 45 | from bpy.props import StringProperty 46 | import subprocess 47 | 48 | import os 49 | 50 | 51 | # need to setup the syspath proprties and operators before loading the rest of the plugin 52 | 53 | class DetectSystem(Operator): 54 | """Detect System (syspath of system python)""" 55 | bl_idname = f"addon_prefs_example.detect_system" 56 | bl_label = f"Detect System" 57 | 58 | def execute(self, context): 59 | cmd = 'python3 -c "import sys; print(sys.path)"' 60 | 61 | addon_prefs = context.preferences.addons[__name__].preferences 62 | 63 | p = subprocess.Popen(cmd, 64 | shell=True, 65 | bufsize=1024, 66 | stdin=subprocess.PIPE, 67 | stderr=subprocess.PIPE, 68 | stdout=subprocess.PIPE) 69 | 70 | for line in p.stdout: 71 | p.stdout.flush() 72 | 73 | addon_prefs.syspath = line.decode('utf8') 74 | 75 | return {'FINISHED'} 76 | 77 | class IntuitionRFAddonPreferences(AddonPreferences): 78 | bl_idname = __name__ 79 | 80 | syspath: StringProperty( 81 | name="System python's syspath", 82 | default="" 83 | ) 84 | 85 | openEMS_directory: StringProperty( 86 | name="openEMS directory (windows only)", 87 | default="", 88 | subtype='DIR_PATH' 89 | ) 90 | 91 | def draw(self, context): 92 | layout = self.layout 93 | layout.label(text="Configure IntuitionRF to your openEMS install") 94 | layout.prop(self, "syspath") 95 | layout.operator(DetectSystem.bl_idname) 96 | layout.prop(self, "openEMS_directory") 97 | 98 | 99 | class OBJECT_OT_IntuitionRFPreferences(Operator): 100 | """IntuitionRF preferences oerator""" 101 | bl_idname = "object.addon_prefs_example" 102 | bl_label = "IntuitionRF system configuration" 103 | bl_options = {'REGISTER', 'UNDO'} 104 | 105 | def execute(self, context): 106 | return {'FINISHED'} 107 | 108 | # make variables for modules 109 | # so we can global import from within register function 110 | #meshing = None 111 | #scene = None 112 | #objects = None 113 | 114 | def register(): 115 | global meshing 116 | global scene 117 | global objects 118 | global geometry_source 119 | 120 | bpy.utils.register_class(DetectSystem) 121 | bpy.utils.register_class(OBJECT_OT_IntuitionRFPreferences) 122 | bpy.utils.register_class(IntuitionRFAddonPreferences) 123 | 124 | addon_prefs = bpy.context.preferences.addons[__name__].preferences 125 | if addon_prefs.syspath == "": 126 | print('Warning : syspath is empty. Skipping addon load') 127 | return 128 | 129 | import ast 130 | syspath = ast.literal_eval(addon_prefs.syspath) 131 | 132 | for item in syspath: 133 | sys.path.append(item) 134 | 135 | # import the dll path 136 | if sys.platform == "win32" and addon_prefs.openEMS_directory != "": 137 | os.add_dll_directory(addon_prefs.openEMS_directory) 138 | 139 | # print(sys.path) 140 | # 141 | if 'meshing' in globals(): #means Blender already started once 142 | import importlib 143 | # print("reimporting") 144 | importlib.reload(meshing) 145 | importlib.reload(scene) 146 | importlib.reload(objects) 147 | importlib.reload(geometry_source) 148 | else: #start up 149 | # print("First time importing") 150 | from . operators import meshing 151 | from . panels import scene, objects 152 | from . nodes import geometry_source 153 | 154 | # register operators 155 | meshing.register() 156 | 157 | # register panels 158 | scene.register() 159 | objects.register() 160 | 161 | geometry_source.register() 162 | 163 | def unregister(): 164 | global objects 165 | global scene 166 | global meshing 167 | 168 | # unregister panels 169 | objects.unregister() 170 | scene.unregister() 171 | geometry_source.unregister() 172 | 173 | # unregister operators 174 | meshing.unregister() 175 | 176 | bpy.utils.unregister_class(DetectSystem) 177 | bpy.utils.unregister_class(OBJECT_OT_IntuitionRFPreferences) 178 | bpy.utils.unregister_class(IntuitionRFAddonPreferences) 179 | 180 | if __name__ == "__main__": 181 | register() 182 | except: 183 | print("Failed to import bpy") 184 | -------------------------------------------------------------------------------- /images/check_for_updates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/check_for_updates.png -------------------------------------------------------------------------------- /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/demo.png -------------------------------------------------------------------------------- /images/panels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/panels.png -------------------------------------------------------------------------------- /images/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/preferences.png -------------------------------------------------------------------------------- /images/reload_scripts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/reload_scripts.png -------------------------------------------------------------------------------- /images/syspath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/syspath.png -------------------------------------------------------------------------------- /images/windows_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_2.png -------------------------------------------------------------------------------- /images/windows_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_3.png -------------------------------------------------------------------------------- /images/windows_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_4.png -------------------------------------------------------------------------------- /images/windows_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_5.png -------------------------------------------------------------------------------- /images/windows_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_6.png -------------------------------------------------------------------------------- /images/windows_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_7.png -------------------------------------------------------------------------------- /images/windows_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_8.png -------------------------------------------------------------------------------- /images/windows_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juleinn/IntuitionRF/e833ea0dafd79886f1769bc64d2c34c27303d33a/images/windows_9.png -------------------------------------------------------------------------------- /make_release.sh: -------------------------------------------------------------------------------- 1 | # script to make a release archive of the plugin 2 | # this is basic enough that we don't need any kind of CI here 3 | 4 | version="$(python3 __init__.py --version)" 5 | rm -rf /tmp/IntuitionRF/ 6 | cp -r "$(pwd)" /tmp/IntuitionRF 7 | # delete everything we don't need in the release package 8 | rm -rf /tmp/IntuitionRF/README.md 9 | rm -rf /tmp/IntuitionRF/__pycache__/ 10 | rm -rf /tmp/IntuitionRF/operators/__pycache__/ 11 | rm -rf /tmp/IntuitionRF/panels/__pycache__/ 12 | rm -rf /tmp/IntuitionRF/images 13 | rm -rf /tmp/IntuitionRF/*blend 14 | rm -rf /tmp/IntuitionRF/*blend1 15 | rm -rf /tmp/IntuitionRF/.git 16 | rm -rf /tmp/IntuitionRF*.zir 17 | 18 | # delete self 19 | rm -rf /tmp/IntuitionRF/make_release.sh 20 | 21 | cd /tmp 22 | zip -r "IntuitionRF-$version-alpha.zip" IntuitionRF/* 23 | -------------------------------------------------------------------------------- /nodes/geometry_source.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import GeometryNodeCustomGroup, IDPropertyWrapPtr, Node, GeometryNode, NodeSocket, NodeTree, GeometryNodeTree, Point 3 | from bpy.props import FloatProperty, PointerProperty, EnumProperty 4 | 5 | class NodeSetPort(bpy.types.GeometryNodeCustomGroup): 6 | """Sets the port options by setting attributes on the points""" 7 | bl_idname = 'NodeSetPort' 8 | bl_label = 'Set Port' 9 | 10 | def init(self, context): 11 | # Create a new node tree for the custom group 12 | self.node_tree = bpy.data.node_groups.new('CustomAttributeNodeTree', 'GeometryNodeTree') 13 | 14 | # Create input and output nodes 15 | input_node = self.node_tree.nodes.new('NodeGroupInput') 16 | output_node = self.node_tree.nodes.new('NodeGroupOutput') 17 | 18 | # Add the Store Named Attribute node 19 | node_store_portindex = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 20 | node_store_portindex.inputs['Name'].default_value = 'intuitionrf.port_index' 21 | node_store_portindex.data_type = 'INT' 22 | node_store_portindex.inputs['Value'].default_value = 1 23 | 24 | # port impedance 25 | node_store_port_impedance = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 26 | node_store_port_impedance.inputs['Name'].default_value = 'intuitionrf.port_impedance' 27 | node_store_port_impedance.data_type = 'FLOAT' 28 | node_store_port_impedance.inputs['Value'].default_value = 50.0 29 | 30 | # port excitation axis 31 | node_store_port_axis = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 32 | node_store_port_axis.inputs['Name'].default_value = 'intuitionrf.port_axis' 33 | node_store_port_axis.data_type = 'FLOAT_VECTOR' 34 | 35 | # port active 36 | node_store_port_active = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 37 | node_store_port_active.inputs['Name'].default_value = 'intuitionrf.port_active' 38 | node_store_port_active.data_type = 'BOOLEAN' 39 | node_store_port_active.inputs['Value'].default_value = False 40 | 41 | # Create links 42 | self.node_tree.interface.new_socket("Geometry", description="Input geometry", in_out="INPUT", socket_type="NodeSocketGeometry") 43 | self.node_tree.interface.new_socket("Index", description="Index of port", in_out="INPUT", socket_type="NodeSocketInt") 44 | self.node_tree.interface.new_socket("Impedance (ohm)", description="Port Impedance (ohm)", in_out="INPUT", socket_type="NodeSocketFloat") 45 | self.node_tree.interface.new_socket("Axis", description="axis", in_out="INPUT", socket_type="NodeSocketVector") 46 | self.node_tree.interface.new_socket("Active", description="Active", in_out="INPUT", socket_type="NodeSocketBool") 47 | self.node_tree.interface.new_socket("Geometry", description="Output geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry") 48 | 49 | # inputs to the nodes 50 | self.node_tree.links.new(input_node.outputs[0], node_store_portindex.inputs[0]) 51 | self.node_tree.links.new(input_node.outputs[1], node_store_portindex.inputs[3]) 52 | self.node_tree.links.new(input_node.outputs[2], node_store_port_impedance.inputs[3]) 53 | self.node_tree.links.new(input_node.outputs[3], node_store_port_axis.inputs[3]) 54 | self.node_tree.links.new(input_node.outputs[4], node_store_port_active.inputs[3]) 55 | 56 | # chain store attribute nodes 57 | self.node_tree.links.new(node_store_portindex.outputs[0], node_store_port_impedance.inputs[0]) 58 | self.node_tree.links.new(node_store_port_impedance.outputs[0], node_store_port_axis.inputs[0]) 59 | self.node_tree.links.new(node_store_port_axis.outputs[0], node_store_port_active.inputs[0]) 60 | 61 | # back to output 62 | self.node_tree.links.new(node_store_port_active.outputs[0], output_node.inputs[0]) 63 | 64 | class NodeSetPEC(bpy.types.GeometryNodeCustomGroup): 65 | """Sets the given geometry as PEC. Can mark edges (will be added as curves) and faces 66 | as volumes, or planes""" 67 | bl_idname = 'NodeSetPEC' 68 | bl_label = 'Set PEC' 69 | 70 | def init(self, context): 71 | # Create a new node tree for the custom group 72 | self.node_tree = bpy.data.node_groups.new('CustomAttributeNodeTree', 'GeometryNodeTree') 73 | 74 | # Create input and output nodes 75 | input_node = self.node_tree.nodes.new('NodeGroupInput') 76 | output_node = self.node_tree.nodes.new('NodeGroupOutput') 77 | 78 | # Add the Store Named Attribute node 79 | node_store_pecedge = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 80 | node_store_pecedge.inputs['Name'].default_value = 'intuitionrf.pec_edge' 81 | node_store_pecedge.data_type = 'BOOLEAN' 82 | node_store_pecedge.inputs['Value'].default_value = True 83 | node_store_pecedge.domain = 'EDGE' 84 | 85 | node_store_aa_faces = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 86 | node_store_aa_faces.inputs['Name'].default_value = 'intuitionrf.pec_aa_face' 87 | node_store_aa_faces.data_type = 'BOOLEAN' 88 | node_store_aa_faces.inputs['Value'].default_value = True 89 | node_store_aa_faces.domain = 'FACE' 90 | 91 | node_store_volume = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 92 | node_store_volume.inputs['Name'].default_value = 'intuitionrf.pec_volume' 93 | node_store_volume.data_type = 'BOOLEAN' 94 | node_store_volume.inputs['Value'].default_value = True 95 | node_store_volume.domain = 'FACE' 96 | 97 | # wiring in-out 98 | self.node_tree.interface.new_socket("Geometry", description="Input geometry", in_out="INPUT", socket_type="NodeSocketGeometry") 99 | self.node_tree.interface.new_socket("Geometry", description="Output geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry") 100 | self.node_tree.interface.new_socket("PEC edge", description="PEC Edge", in_out="INPUT", socket_type="NodeSocketBool") 101 | self.node_tree.interface.new_socket("AA faces", description="AA faces", in_out="INPUT", socket_type="NodeSocketBool") 102 | self.node_tree.interface.new_socket("Volume", description="Volume", in_out="INPUT", socket_type="NodeSocketBool") 103 | 104 | # wire inputs 105 | self.node_tree.links.new(input_node.outputs[0], node_store_pecedge.inputs[0]) 106 | self.node_tree.links.new(input_node.outputs[1], node_store_pecedge.inputs[3]) 107 | self.node_tree.links.new(input_node.outputs[2], node_store_aa_faces.inputs[3]) 108 | self.node_tree.links.new(input_node.outputs[3], node_store_volume.inputs[3]) 109 | 110 | # chain link attribute node 111 | self.node_tree.links.new(node_store_pecedge.outputs[0], node_store_aa_faces.inputs[0]) 112 | self.node_tree.links.new(node_store_aa_faces.outputs[0], node_store_volume.inputs[0]) 113 | 114 | # link to output 115 | self.node_tree.links.new(node_store_volume.outputs[0], output_node.inputs[0]) 116 | 117 | class NodeSetMaterial(bpy.types.GeometryNodeCustomGroup): 118 | """Marks the given geometry (face attributes) as a material with epsilon and optional kappa""" 119 | bl_idname = 'NodeSetMaterial' 120 | bl_label = 'Set Material' 121 | 122 | def init(self, context): 123 | # Create a new node tree for the custom group 124 | self.node_tree = bpy.data.node_groups.new('CustomAttributeNodeTree', 'GeometryNodeTree') 125 | 126 | # Create input and output nodes 127 | input_node = self.node_tree.nodes.new('NodeGroupInput') 128 | output_node = self.node_tree.nodes.new('NodeGroupOutput') 129 | 130 | # non-material vertices will have a epsilon value of 0 (impossible in the real world, where minimum is 1) 131 | node_store_material_epsilon = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 132 | node_store_material_epsilon.inputs['Name'].default_value = 'intuitionrf.epsilonr' 133 | node_store_material_epsilon.data_type = 'FLOAT' 134 | node_store_material_epsilon.inputs['Value'].default_value = 1.0 135 | node_store_material_epsilon.domain = 'FACE' 136 | 137 | node_store_material_use_kappa = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 138 | node_store_material_use_kappa.inputs['Name'].default_value = 'intuitionrf.use_kappa' 139 | node_store_material_use_kappa.data_type = 'BOOLEAN' 140 | node_store_material_use_kappa.inputs['Value'].default_value = True 141 | node_store_material_use_kappa.domain = 'FACE' 142 | 143 | node_store_material_kappa = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 144 | node_store_material_kappa.inputs['Name'].default_value = 'intuitionrf.kappa' 145 | node_store_material_kappa.data_type = 'FLOAT' 146 | node_store_material_kappa.inputs['Value'].default_value = True 147 | node_store_material_kappa.domain = 'FACE' 148 | 149 | self.node_tree.interface.new_socket("Geometry", description="Input geometry", in_out="INPUT", socket_type="NodeSocketGeometry") 150 | self.node_tree.interface.new_socket("Geometry", description="Output geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry") 151 | self.node_tree.interface.new_socket("EpsilonR", description="EpsilonR", in_out="INPUT", socket_type="NodeSocketFloat") 152 | self.node_tree.interface.new_socket("Use Kappa", description="Use Kappa", in_out="INPUT", socket_type="NodeSocketBool") 153 | self.node_tree.interface.new_socket("Kappa", description="Kappa", in_out="INPUT", socket_type="NodeSocketFloat") 154 | 155 | # input 156 | self.node_tree.links.new(input_node.outputs[0], node_store_material_epsilon.inputs[0]) 157 | self.node_tree.links.new(input_node.outputs[1], node_store_material_epsilon.inputs[3]) 158 | self.node_tree.links.new(input_node.outputs[2], node_store_material_use_kappa.inputs[3]) 159 | self.node_tree.links.new(input_node.outputs[3], node_store_material_kappa.inputs[3]) 160 | 161 | # chain attribute nodes 162 | self.node_tree.links.new(node_store_material_epsilon.outputs[0], node_store_material_use_kappa.inputs[0]) 163 | self.node_tree.links.new(node_store_material_use_kappa.outputs[0], node_store_material_kappa.inputs[0]) 164 | 165 | # output 166 | self.node_tree.links.new(node_store_material_kappa.outputs[0], output_node.inputs[0]) 167 | 168 | class NodeSetAnchor(bpy.types.GeometryNodeCustomGroup): 169 | """Marks the given geometry (vertices) as anchors for OpenEMS meshing""" 170 | bl_idname = 'NodeSetAnchor' 171 | bl_label = 'Set Anchor' 172 | 173 | def init(self, context): 174 | # Create a new node tree for the custom group 175 | self.node_tree = bpy.data.node_groups.new('CustomAttributeNodeTree', 'GeometryNodeTree') 176 | 177 | # Create input and output nodes 178 | input_node = self.node_tree.nodes.new('NodeGroupInput') 179 | output_node = self.node_tree.nodes.new('NodeGroupOutput') 180 | 181 | node_store_is_anchor = self.node_tree.nodes.new('GeometryNodeStoreNamedAttribute') 182 | node_store_is_anchor.inputs['Name'].default_value = 'intuitionrf.anchor' 183 | node_store_is_anchor.data_type = 'BOOLEAN' 184 | node_store_is_anchor.inputs['Value'].default_value = True 185 | 186 | # I/O 187 | self.node_tree.interface.new_socket("Geometry", description="Input geometry", in_out="INPUT", socket_type="NodeSocketGeometry") 188 | self.node_tree.interface.new_socket("Is anchor", description="Is anchor", in_out="INPUT", socket_type="NodeSocketBool") 189 | self.node_tree.interface.new_socket("Geometry", description="Output geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry") 190 | 191 | self.node_tree.links.new(input_node.outputs[0], node_store_is_anchor.inputs[0]) 192 | self.node_tree.links.new(input_node.outputs[0], node_store_is_anchor.inputs[3]) 193 | self.node_tree.links.new(node_store_is_anchor.outputs[0], output_node.inputs[0]) 194 | 195 | 196 | def draw_node_menu(self, context): 197 | layout = self.layout 198 | layout.operator('node.add_node', text="IntuitionRF port").type = NodeSetPort.bl_idname 199 | layout.operator('node.add_node', text="IntuitionRF PEC").type = NodeSetPEC.bl_idname 200 | layout.operator('node.add_node', text="IntuitionRF Material").type = NodeSetMaterial.bl_idname 201 | layout.operator('node.add_node', text="IntuitionRF Anchor").type = NodeSetAnchor.bl_idname 202 | 203 | # Register the custom node 204 | def register(): 205 | print('register nodes') 206 | # register sockets 207 | # bpy.utils.register_class(IRFPrimitiveSocket) 208 | 209 | # register nodes 210 | bpy.utils.register_class(NodeSetPort) 211 | bpy.utils.register_class(NodeSetPEC) 212 | bpy.utils.register_class(NodeSetMaterial) 213 | bpy.utils.register_class(NodeSetAnchor) 214 | 215 | # register node menu 216 | bpy.types.NODE_MT_add.append(draw_node_menu) 217 | 218 | def unregister(): 219 | # unregister menu 220 | bpy.types.NODE_MT_add.remove(draw_node_menu) 221 | 222 | # unregister node nodes 223 | bpy.utils.unregister_class(NodeSetPort) 224 | bpy.utils.unregister_class(NodeSetPEC) 225 | bpy.utils.unregister_class(NodeSetMaterial) 226 | bpy.utils.unregister_class(NodeSetAnchor) 227 | 228 | # unregister sockets 229 | # bpy.utils.unregister_class(IRFPrimitiveSocket) 230 | -------------------------------------------------------------------------------- /operators/convert.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # workaround a bug in vtk/or python interpreter bundled with blender 3 | from unittest.mock import MagicMock 4 | 5 | sys.modules['vtkmodules.vtkRenderingMatplotlib'] = MagicMock() 6 | import vtk 7 | from vtk.util.numpy_support import vtk_to_numpy 8 | import numpy as np 9 | import scipy 10 | import pyopenvdb as vdb 11 | from scipy.interpolate import RegularGridInterpolator 12 | from scipy.ndimage import convolve, gaussian_filter 13 | import glob 14 | import threading 15 | import time 16 | import multiprocessing 17 | import os 18 | 19 | def vtr_to_vdb(vtr_file, vdb_file, dicing_factor=8): 20 | # read input data 21 | reader = vtk.vtkXMLRectilinearGridReader() 22 | reader.SetFileName(vtr_file) 23 | reader.Update() 24 | 25 | output = reader.GetOutput() 26 | 27 | point_data = output.GetPointData() 28 | a0 = point_data.GetArray(0) 29 | points = vtk.vtkPoints() 30 | output.GetPoints(points) 31 | 32 | point_data = vtk_to_numpy(a0) 33 | point_data_magnitude = np.linalg.norm(point_data, axis=1) 34 | points = vtk_to_numpy(points.GetData()) 35 | 36 | 37 | # get irregular grid axes 38 | points_x = points[:,0] 39 | points_y = points[:,1] 40 | points_z = points[:,2] 41 | 42 | # get the offset for the grid 43 | offset_x = np.min(points_x) 44 | offset_y = np.min(points_y) 45 | offset_z = np.min(points_z) 46 | 47 | # get the spacing of the axes 48 | dx = np.diff(points_x) 49 | dy = np.diff(points_y) 50 | dz = np.diff(points_z) 51 | 52 | # non-zero spacing 53 | dx = dx[dx != 0] 54 | dy = dy[dy != 0] 55 | dz = dz[dz != 0] 56 | 57 | # get the smallest cell-dimension in the grid 58 | minx = np.min(np.abs(dx)) 59 | miny = np.min(np.abs(dy)) 60 | minz = np.min(np.abs(dz)) 61 | cell_size = min(minx, miny, minz) 62 | 63 | # artificially force cell size up to reduce interpolation time 64 | cell_size *= dicing_factor 65 | 66 | # irregular grid 67 | grid_dim_x = int((np.max(points_x) - np.min(points_x)) / cell_size) 68 | grid_dim_y = int((np.max(points_y) - np.min(points_y)) / cell_size) 69 | grid_dim_z = int((np.max(points_z) - np.min(points_z)) / cell_size) 70 | 71 | grid_x = np.linspace(np.min(points_x), np.max(points_x), grid_dim_x) 72 | grid_y = np.linspace(np.min(points_y), np.max(points_y), grid_dim_y) 73 | grid_z = np.linspace(np.min(points_z), np.max(points_z), grid_dim_z) 74 | 75 | # Create an interpolator 76 | ux = np.unique(points_x) 77 | uy = np.unique(points_y) 78 | uz = np.unique(points_z) 79 | # this is the correct input data 80 | point_data_magnitude = point_data_magnitude.reshape(len(uz), len(uy), len(ux)).T 81 | interpolator = RegularGridInterpolator((ux, uy, uz), point_data_magnitude) 82 | 83 | # Generate the regular grid for interpolation 84 | regular_points = np.array(np.meshgrid(grid_x, grid_y, grid_z, indexing='ij')).T 85 | regular_points = regular_points.reshape(-1, 3) 86 | 87 | # Interpolation 88 | interpolated_values = interpolator(regular_points) 89 | #print('\ninterpolation complete') 90 | #print(f"{interpolated_values.shape=}") 91 | 92 | # check if there is NaN values in the array 93 | 94 | # Reshape result 95 | interpolated_volume = interpolated_values.reshape(len(grid_z), len(grid_y), len(grid_x)).T 96 | 97 | # log view of the output because of massive scale between min and max values 98 | do_log = 1 99 | if do_log: 100 | interpolated_volume = np.log(interpolated_volume, 101 | out=np.zeros_like(interpolated_volume), 102 | where=interpolated_volume!=0) 103 | # need to add the minimimum to the log'd values because otherwise we get negative attributes 104 | # from values <1 pre-log 105 | # need the minimum accross all frames 106 | min_value = 15 107 | #print(f"{min_value=}") 108 | interpolated_volume[interpolated_volume != 0] += min_value 109 | # remove negative values all together 110 | interpolated_volume[interpolated_volume < 0] = 0 111 | # reduce the total output range 112 | interpolated_volume *= .01 113 | 114 | # now we add a peak detection using sobel filtering 115 | # filters kernels 116 | # this largely came from ChatGPT 117 | sobel_x = np.array([[[1, 0, -1], [2, 0, -2], [1, 0, -1]], 118 | [[2, 0, -2], [4, 0, -4], [2, 0, -2]], 119 | [[1, 0, -1], [2, 0, -2], [1, 0, -1]]]) 120 | sobel_y = np.array([[[1, 2, 1], [0, 0, 0], [-1, -2, -1]], 121 | [[2, 4, 2], [0, 0, 0], [-2, -4, -2]], 122 | [[1, 2, 1], [0, 0, 0], [-1, -2, -1]]]) 123 | sobel_z = np.array([[[1, 2, 1], [2, 4, 2], [1, 2, 1]], 124 | [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 125 | [[-1, -2, -1], [-2, -4, -2], [-1, -2, -1]]]) 126 | 127 | sobel_x = np.array([ 128 | [[-1, 0, 1], [-3, 0, 3], [-1, 0, 1]], 129 | [[-3, 0, 3], [-6, 0, 6], [-3, 0, 3]], 130 | [[-1, 0, 1], [-3, 0, 3], [-1, 0, 1]] 131 | ]) 132 | 133 | sobel_y = np.array([ 134 | [[-1, -3, -1], [ 0, 0, 0], [ 1, 3, 1]], 135 | [[-3, -6, -3], [ 0, 0, 0], [ 3, 6, 3]], 136 | [[-1, -3, -1], [ 0, 0, 0], [ 1, 3, 1]] 137 | ]) 138 | 139 | sobel_z = np.array([ 140 | [[-1, -3, -1], [-3, -6, -3], [-1, -3, -1]], 141 | [[ 0, 0, 0], [ 0, 0, 0], [ 0, 0, 0]], 142 | [[ 1, 3, 1], [ 3, 6, 3], [ 1, 3, 1]] 143 | ]) 144 | 145 | 146 | #interpolated_volume_smoothed = gaussian_filter(interpolated_volume, sigma=10, radius=25) 147 | interpolated_volume_smoothed = interpolated_volume 148 | time_start = time.time() 149 | gradient_x = convolve(interpolated_volume_smoothed, sobel_x, mode='reflect') 150 | gradient_y = convolve(interpolated_volume_smoothed, sobel_y, mode='reflect') 151 | gradient_z = convolve(interpolated_volume_smoothed, sobel_z, mode='reflect') 152 | gradient_x = np.abs(gradient_x) 153 | gradient_y = np.abs(gradient_y) 154 | gradient_z = np.abs(gradient_z) 155 | 156 | peaks_x = np.zeros_like(interpolated_volume_smoothed, dtype=bool) 157 | peaks_y = np.zeros_like(interpolated_volume_smoothed, dtype=bool) 158 | peaks_z = np.zeros_like(interpolated_volume_smoothed, dtype=bool) 159 | 160 | # remove first element, get positives, remove last element, get negatives 161 | # were they are both true its a peak 162 | peaks_x[:-1, :, :] = (gradient_x[:-1, :, :] > 0) & (gradient_x[1:, :, :] < 0) 163 | peaks_y[:, :-1, :] = (gradient_y[:, :-1, :] > 0) & (gradient_y[:, 1:, :] < 0) 164 | peaks_z[:, :, :-1] = (gradient_z[:, :, :-1] > 0) & (gradient_z[:, :, 1:] < 0) 165 | 166 | 167 | # Combine peaks from all directions 168 | peaks = peaks_x + peaks_y + peaks_z 169 | peaks = peaks.astype(float) 170 | peaks_x = peaks_x.astype(float) 171 | peaks_y = peaks_y.astype(float) 172 | peaks_z = peaks_z.astype(float) 173 | peaks = gaussian_filter(peaks, sigma=2, radius=2) 174 | 175 | ### -------------------- 176 | ### VDB stuff now 177 | ### -------------------- 178 | 179 | vdb_grid = vdb.FloatGrid() 180 | #vdb_grid.background = 0.0 181 | vdb_grid.name = 'magnitude' 182 | 183 | sobel_grid = vdb.FloatGrid() 184 | sobel_grid.name = 'sobel' 185 | 186 | sobel_grid_x = vdb.FloatGrid() 187 | sobel_grid_x.name = 'sobel_x' 188 | sobel_grid_y = vdb.FloatGrid() 189 | sobel_grid_y.name = 'sobel_y' 190 | sobel_grid_z = vdb.FloatGrid() 191 | sobel_grid_z.name = 'sobel_z' 192 | 193 | accessor = vdb_grid.getAccessor() 194 | sobel_accessor = sobel_grid.getAccessor() 195 | sobel_accessor_x = sobel_grid_x.getAccessor() 196 | sobel_accessor_y = sobel_grid_y.getAccessor() 197 | sobel_accessor_z = sobel_grid_z.getAccessor() 198 | 199 | # compute the scale factor 200 | scale_factor = 1 / cell_size 201 | print(f"scale_factor = {scale_factor} ( scale down by a factor of {cell_size})") 202 | 203 | for index_x, x in enumerate(grid_x): 204 | for index_y, y in enumerate(grid_y): 205 | for index_z, z in enumerate(grid_z): 206 | accessor.setValueOn((index_x, index_y, index_z), 207 | interpolated_volume[index_x][index_y][index_z]) 208 | sobel_accessor.setValueOn((index_x, index_y, index_z), 209 | gradient_x[index_x][index_y][index_z] + 210 | gradient_y[index_x][index_y][index_z] + 211 | gradient_z[index_x][index_y][index_z]) 212 | sobel_accessor_x.setValueOn((index_x, index_y, index_z), 213 | gradient_x[index_x][index_y][index_z]) 214 | sobel_accessor_y.setValueOn((index_x, index_y, index_z), 215 | gradient_y[index_x][index_y][index_z]) 216 | sobel_accessor_z.setValueOn((index_x, index_y, index_z), 217 | gradient_z[index_x][index_y][index_z]) 218 | 219 | 220 | #print(f"write {vdb_file}") 221 | vdb.write(vdb_file, grids=[vdb_grid, sobel_grid, sobel_grid_x, sobel_grid_y, sobel_grid_z]) 222 | 223 | return scale_factor, (offset_x, offset_y, offset_z) 224 | 225 | def thread_func(args): 226 | file_split, basename, dicing_factor = args 227 | scale_factor = 1 228 | offset = (0,0,0) 229 | for local_index, (index, file_vtr) in enumerate(file_split): 230 | print(file_vtr) 231 | file_vdb = f"{basename}_{int(index):06d}.vdb" 232 | scale_factor, offset = vtr_to_vdb(file_vtr, os.path.join(os.path.dirname(file_vtr), file_vdb), dicing_factor) 233 | print(f"processed {int((local_index+1)/len(file_split)*100)}% of thread split") 234 | 235 | return scale_factor, offset 236 | 237 | def run_parrallel(args, thread_count): 238 | with multiprocessing.Pool(processes=thread_count) as pool: 239 | results = pool.map(thread_func, args) 240 | 241 | return results 242 | -------------------------------------------------------------------------------- /operators/meshing.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import sys 3 | import bmesh 4 | import mathutils 5 | from mathutils import geometry 6 | import os 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import math 10 | import glob 11 | from collections import defaultdict 12 | from . import convert 13 | from .convert import run_parrallel 14 | import multiprocessing 15 | 16 | # workaround a bug in vtk/or python interpreter bundled with blender 17 | from unittest.mock import MagicMock 18 | 19 | from ..panels.scene import update_port_list 20 | sys.modules['vtkmodules.vtkRenderingMatplotlib'] = MagicMock() 21 | import vtk 22 | 23 | from CSXCAD import CSXCAD, CSPrimitives 24 | from openEMS import openEMS 25 | from openEMS.physical_constants import * 26 | 27 | # workaround for blender not letting me register a LumpedPort to 28 | # a blender object (probably for serialization ?) 29 | # Its OK to loose thoose references on blender exit (for now) 30 | from collections import defaultdict 31 | ports = defaultdict(lambda: None) 32 | nf2ff = None 33 | 34 | import tempfile 35 | 36 | def extract_lines_xyz(lines): 37 | mesh = lines.data 38 | verts = mesh.vertices 39 | edges = mesh.edges 40 | 41 | x = set() 42 | y = set() 43 | z = set() 44 | 45 | for edge in edges: 46 | if verts[edge.vertices[0]].co[0] != verts[edge.vertices[1]].co[0]: 47 | x.add(verts[edge.vertices[0]].co[0]) 48 | x.add(verts[edge.vertices[1]].co[0]) 49 | if verts[edge.vertices[0]].co[1] != verts[edge.vertices[1]].co[1]: 50 | y.add(verts[edge.vertices[0]].co[1]) 51 | y.add(verts[edge.vertices[1]].co[1]) 52 | if verts[edge.vertices[0]].co[2] != verts[edge.vertices[1]].co[2]: 53 | z.add(verts[edge.vertices[0]].co[2]) 54 | z.add(verts[edge.vertices[1]].co[2]) 55 | 56 | return (x, y, z) 57 | 58 | def add_meshline(context, direction): 59 | bpy.ops.object.mode_set(mode='OBJECT') 60 | # backup which object we're currently editing 61 | source_object = context.view_layer.objects.active 62 | 63 | # backup source selected vertices to slice at 64 | source_selected_verts = [v for v in bpy.context.active_object.data.vertices if v.select] 65 | 66 | bm = bmesh.new() 67 | bm.from_mesh(context.scene.intuitionRF_lines.data) 68 | 69 | # switch to the lines object 70 | for vert in source_selected_verts: 71 | for edge in bm.edges: 72 | # compute intersection points between face (selected vert, normal +x) 73 | # and every edge in meshing the lines 74 | v1, v2 = edge.verts 75 | intersection = geometry.intersect_line_plane(v1.co, v2.co, vert.co, direction) 76 | if intersection is not None: 77 | # exclude out of bounds hit 78 | if (v2.co - v1.co).length_squared > (intersection - v1.co).length_squared and \ 79 | (v2.co - v1.co).length_squared > (intersection - v2.co).length_squared: 80 | # add intersection as new vertex 81 | new_vertex = bm.verts.new(intersection) 82 | bm.verts.index_update() 83 | 84 | new_edge_1 = bm.edges.new((v1, new_vertex)) 85 | new_edge_2 = bm.edges.new((new_vertex, v2)) 86 | # add 2 edges from v1 to intersection and v2 to intersection 87 | 88 | # delete old existing edge 89 | bmesh.ops.delete(bm, geom=[edge], context='EDGES') 90 | 91 | bm.verts.index_update() 92 | bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=.000001) 93 | 94 | bm.to_mesh(context.scene.intuitionRF_lines.data) 95 | bm.free() 96 | # switch back to the source object 97 | source_object = context.view_layer.objects.active 98 | # switch back to edit mode 99 | bpy.ops.object.mode_set(mode='EDIT') 100 | 101 | class IntuitionRF_OT_add_meshline_x(bpy.types.Operator): 102 | """ IntuitionRF : edit mode operator, gets the selected vertices, and slices the 103 | mesh lines at the x projection of vertex to add line at the given position """ 104 | bl_idname = "intuitionrf.add_meshline_x" 105 | bl_label = "Add meshline x" 106 | 107 | def execute(self, context): 108 | vec_x = mathutils.Vector((1.0, 0.0, 0.0)) 109 | add_meshline(context, vec_x) 110 | 111 | return {"FINISHED"} 112 | 113 | class IntuitionRF_OT_add_meshline_y(bpy.types.Operator): 114 | """ IntuitionRF : edit mode operator, gets the selected vertices, and slices the 115 | mesh lines at the y projection of vertex to add line at the given position """ 116 | bl_idname = "intuitionrf.add_meshline_y" 117 | bl_label = "Add meshline y" 118 | 119 | def execute(self, context): 120 | vec_y = mathutils.Vector((0.0, 1.0, 0.0)) 121 | add_meshline(context, vec_y) 122 | 123 | return {"FINISHED"} 124 | 125 | class IntuitionRF_OT_add_meshline_z(bpy.types.Operator): 126 | """ IntuitionRF : edit mode operator, gets the selected vertices, and slices the 127 | mesh lines at the z projection of vertex to add line at the given position """ 128 | bl_idname = "intuitionrf.add_meshline_z" 129 | bl_label = "Add meshline z" 130 | 131 | def execute(self, context): 132 | vec_z = mathutils.Vector((0.0, 0.0, 1.0)) 133 | add_meshline(context, vec_z) 134 | 135 | return {"FINISHED"} 136 | 137 | class IntuitionRF_OT_add_domain(bpy.types.Operator): 138 | """ Add a IntuitionRF simulation domain """ 139 | bl_idname = "intuitionrf.add_domain" 140 | bl_label = "Add a \u03BB/2 RF simulation domain" 141 | 142 | def execute(self, context): 143 | bpy.ops.mesh.primitive_cube_add() 144 | cube = context.active_object 145 | cube.name = "IntuitionRF_domain" 146 | bpy.context.view_layer.objects.active = cube 147 | bpy.ops.object.mode_set(mode='EDIT') 148 | bpy.ops.mesh.normals_make_consistent(inside=False) 149 | bpy.ops.object.mode_set(mode='OBJECT') 150 | # lambda = c/f ~= 300/MHz 151 | wavelength_over_2 = .5 * 300 / context.scene.center_freq 152 | # default cube is twice as big as the unit cube 153 | bpy.ops.transform.resize(value=(.5 * wavelength_over_2,.5 * wavelength_over_2,.5 * wavelength_over_2)) 154 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 155 | cube.display_type = 'WIRE' 156 | cube.show_name = True 157 | context.scene.intuitionRF_domain = cube 158 | self.report({'INFO'}, "Custom function executed!") 159 | return {"FINISHED"} 160 | 161 | class IntuitionRF_OT_add_wavelength_cube(bpy.types.Operator): 162 | """ Add a IntuitionRF simulation domain """ 163 | bl_idname = "intuitionrf.add_wavelength_cube" 164 | bl_label = "Add a \u03BB/20 reference cube" 165 | 166 | def execute(self, context): 167 | bpy.ops.mesh.primitive_cube_add() 168 | cube = context.active_object 169 | cube.name = "wavelength_over_20" 170 | bpy.context.view_layer.objects.active = cube 171 | bpy.ops.object.mode_set(mode='EDIT') 172 | bpy.ops.mesh.normals_make_consistent(inside=False) 173 | bpy.ops.object.mode_set(mode='OBJECT') 174 | # lambda = c/f ~= 300/MHz 175 | wavelength_over_20 = (300 / context.scene.center_freq) / 20 176 | # default cube is twice as big as the unit cube 177 | bpy.ops.transform.resize(value=(.5 * wavelength_over_20,.5 * wavelength_over_20,.5 * wavelength_over_20)) 178 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 179 | cube.display_type = 'WIRE' 180 | cube.show_name = True 181 | context.scene.intuitionRF_domain = cube 182 | return {"FINISHED"} 183 | 184 | class IntuitionRF_OT_add_default_lines(bpy.types.Operator): 185 | """ Add a IntuitionRF default meshing lines set """ 186 | bl_idname = "intuitionrf.add_default_lines" 187 | bl_label = "Add a default meshing line set of \u03BB/2" 188 | 189 | def execute(self, context): 190 | mesh = bpy.data.meshes.new("mesh") # add a new mesh 191 | lines = bpy.data.objects.new("lines", mesh) # add a new object using the mesh 192 | bpy.context.collection.objects.link(lines) 193 | bpy.context.view_layer.objects.active = lines 194 | 195 | wavelength_over_2 = 300 / bpy.context.scene.center_freq 196 | 197 | verts = [(-wavelength_over_2 / 2, -wavelength_over_2 / 2, -wavelength_over_2 / 2), 198 | (wavelength_over_2 / 2, -wavelength_over_2 / 2, -wavelength_over_2 / 2), 199 | (-wavelength_over_2 / 2, wavelength_over_2 / 2, -wavelength_over_2 / 2), 200 | (-wavelength_over_2 / 2, -wavelength_over_2 / 2, wavelength_over_2 / 2), 201 | ] 202 | edges = [[0, 1], [0, 1], [0, 2], [0, 3]] 203 | faces = [] 204 | mesh.from_pydata(verts, edges, faces) 205 | lines.show_name = True 206 | 207 | context.scene.intuitionRF_lines = lines 208 | # lets set the default smoothing max res to lambda/40 209 | context.scene.intuitionRF_smooth_max_res = wavelength_over_2 / 20 210 | return {"FINISHED"} 211 | 212 | class IntuitionRF_OT_preview_CSX(bpy.types.Operator): 213 | """Preview SIM from current configuration in CSXCAD""" 214 | bl_idname = "intuitionrf.preview_csx" 215 | bl_label = "Preview sim in CSXCAD" 216 | 217 | def execute(self, context): 218 | FDTD = openEMS(NrTS=1, EndCriteria=1e-4) 219 | 220 | 221 | CSX = CSXCAD.ContinuousStructure() 222 | CSX = meshlines_from_scene(CSX, context) 223 | FDTD.SetCSX(CSX) 224 | FDTD.SetGaussExcite( context.scene.center_freq * 1e6, context.scene.cutoff_freq * 1e6) 225 | FDTD.SetBoundaryCond( ['MUR', 'MUR', 'MUR', 'MUR', 'MUR', 'PML_8'] ) 226 | 227 | FDTD, CSX = objects_from_scene(FDTD, CSX, context) 228 | 229 | mesh = CSX.GetGrid() 230 | mesh_res = context.scene.intuitionRF_smooth_max_res 231 | mesh.SmoothMeshLines('all', mesh_res, 1.4) 232 | 233 | # create temporary dir 234 | tmp_dir = tempfile.mkdtemp() 235 | CSX_file = f"{tmp_dir}/meshing.xml" 236 | CSX.Write2XML(CSX_file) 237 | 238 | from CSXCAD import AppCSXCAD_BIN 239 | os.system(AppCSXCAD_BIN + ' "{}"'.format(CSX_file)) 240 | 241 | return {"FINISHED"} 242 | 243 | class IntuitionRF_OT_preview_PEC_dump(bpy.types.Operator): 244 | """Preview SIM from current configuration in CSXCAD""" 245 | bl_idname = "intuitionrf.preview_pec_dump" 246 | bl_label = "View PEC dump" 247 | 248 | def execute(self, context): 249 | FDTD = openEMS(NrTS=1, EndCriteria=1e-4) 250 | 251 | CSX = CSXCAD.ContinuousStructure() 252 | CSX = meshlines_from_scene(CSX, context) 253 | FDTD.SetCSX(CSX) 254 | FDTD.SetGaussExcite( context.scene.center_freq * 1e6, context.scene.cutoff_freq * 1e6) 255 | FDTD.SetBoundaryCond( ['MUR', 'MUR', 'MUR', 'MUR', 'MUR', 'PML_8'] ) 256 | 257 | FDTD, CSX = objects_from_scene(FDTD, CSX, context) 258 | 259 | #mesh = CSX.GetGrid() 260 | #mesh_res = context.scene.intuitionRF_smooth_max_res 261 | #mesh.SmoothMeshLines('all', mesh_res, 1.4) 262 | 263 | # dry run the SIM 264 | FDTD.Run(sim_path=context.scene.intuitionRF_simdir, cleanup=False, setup_only=True, debug_material=True, debug_pec=True) 265 | 266 | # now import the meshing lines from the output VTP file 267 | PEC_filename = f"{context.scene.intuitionRF_simdir}/PEC_dump.vtp" 268 | PEC_dump_to_scene(PEC_filename, context) 269 | 270 | return {"FINISHED"} 271 | 272 | class IntuitionRF_OT_run_sim(bpy.types.Operator): 273 | """Run the currently defined simulation in OpenEMS""" 274 | bl_idname = "intuitionrf.run_sim" 275 | bl_label = "Run SIM" 276 | 277 | def execute(self, context): 278 | run_sim(context) 279 | return {"FINISHED"} 280 | 281 | def run_sim(context): 282 | global nf2ff 283 | FDTD = openEMS(NrTS=1e6, EndCriteria=1e-4) 284 | if context.scene.intuitionRF_oversampling > 1: 285 | FDTD.SetOverSampling(context.scene.intuitionRF_oversampling) 286 | 287 | CSX = CSXCAD.ContinuousStructure() 288 | CSX = meshlines_from_scene(CSX, context) 289 | FDTD.SetCSX(CSX) 290 | if context.scene.intuitionRF_excitation_type == "gauss": 291 | FDTD.SetGaussExcite( context.scene.center_freq * 1e6, context.scene.cutoff_freq * 1e6) 292 | elif context.scene.intuitionRF_excitation_type == "custom": 293 | FDTD.SetCustomExcite( context.scene.intuitionRF_excitation_custom_function, context.scene.center_freq * 1e6, context.scene.cutoff_freq * 1e6) 294 | else: 295 | FDTD.SetSinusExcite( context.scene.center_freq * 1e6) 296 | 297 | FDTD.SetBoundaryCond( ['MUR', 'MUR', 'MUR', 'MUR', 'MUR', 'MUR'] ) 298 | 299 | FDTD, CSX = objects_from_scene(FDTD, CSX, context) 300 | 301 | mesh = CSX.GetGrid() 302 | mesh_res = context.scene.intuitionRF_smooth_max_res 303 | mesh.SmoothMeshLines('all', mesh_res, 1.4) 304 | 305 | # Add the nf2ff recording box 306 | nf2ff = FDTD.CreateNF2FFBox() 307 | 308 | FDTD.Run(sim_path=context.scene.intuitionRF_simdir, cleanup=False) 309 | 310 | update_port_list(ports) 311 | 312 | return (FDTD, CSX, nf2ff, ports) 313 | 314 | class IntuitionRF_OT_compute_NF2FF(bpy.types.Operator): 315 | """Compute the near field to far field at resonnant frequency""" 316 | bl_idname = "intuitionrf.compute_nf2ff" 317 | bl_label = "Compute NF2FF" 318 | 319 | def execute(self, context): 320 | global nf2ff 321 | if nf2ff == None: 322 | self.report({'INFO'}, "NF2FF does not exist. (re)run sim") 323 | return {"FINISHED"} 324 | 325 | 326 | self.report({'INFO'}, "Computing NF2FF. This may take some time") 327 | step_degrees = 5.0 328 | step_count_theta = int(180 / step_degrees) 329 | step_count_phi = int(360 / step_degrees) 330 | theta = np.arange(-180.0, 180.0, step_degrees) 331 | phi = np.arange(-360.0, 360.0, step_degrees) 332 | nf2ff_res = nf2ff.CalcNF2FF( 333 | sim_path=context.scene.intuitionRF_simdir, 334 | freq=context.scene.intuitionRF_resonnant_freq * 1e6, 335 | theta=theta, 336 | phi=phi, 337 | center=[0,0,0] 338 | ) 339 | 340 | # phi = 0 => x-z plane 341 | # theta = 90 => x-y plane 342 | # phi 'horizontal' starting at X axis 343 | # theta 'vertical' starting at Z axis 344 | 345 | Dmax_dB = 10*np.log10(nf2ff_res.Dmax[0]) 346 | # don't know why add directivity but seems to be the way 347 | # E_norm[0] -> frequency index 0 (only on here) 348 | E_norm = 20.0*np.log10(nf2ff_res.E_norm[0]/np.max(nf2ff_res.E_norm[0])) \ 349 | + 10*np.log10(nf2ff_res.Dmax[0]) 350 | 351 | E_min = np.min(E_norm) 352 | 353 | # E_norm[theta][phi] 354 | # 355 | # create a new mesh 356 | mesh = bpy.data.meshes.new("rad_pattern") # add a new mesh 357 | rad_pat = bpy.data.objects.new(f"radiation_pattern_peak_{nf2ff_res.Dmax[0]:.2f}_dBi", mesh) # add a new object using the mesh 358 | bpy.context.collection.objects.link(rad_pat) 359 | 360 | # draw lines in each directions 361 | verts = [] 362 | edges = [] 363 | faces = [] 364 | for it, t in enumerate(theta * math.pi / 180): 365 | for ip, p in enumerate(phi * math.pi / 180): 366 | # can probably do this numpy-way but we need the indices for meshing 367 | 368 | # need to offset such that there is no negative gain furthur out than pos 369 | norm = E_norm[it][ip] - E_min 370 | # norm = 1 371 | x = math.sin(t) * math.cos(p) * norm 372 | y = math.sin(t) * math.sin(p) * norm 373 | z = math.cos(t) * norm 374 | 375 | # scaling factor here ? 376 | 377 | verts.append(tuple((x, y, z))) 378 | 379 | for it, _ in enumerate(theta[:-1]): 380 | for ip, _ in enumerate(phi[:-1]): 381 | i0 = (it * step_count_phi) + ip 382 | i1 = (it * step_count_phi) + ip + 1 383 | i2 = ((it+1) * step_count_phi) + ip 384 | i3 = ((it+1) * step_count_phi) + ip + 1 385 | 386 | faces.append([i0, i1, i3, i2]) 387 | 388 | mesh.from_pydata(verts, edges, faces) 389 | mesh.validate(verbose=True) 390 | 391 | # do a uv-sphere meshing here 392 | 393 | self.report({'INFO'}, "Complete") 394 | 395 | return {"FINISHED"} 396 | 397 | class IntuitionRF_OT_check_updates(bpy.types.Operator): 398 | """Check for plugin updates""" 399 | bl_idname = "intuitionrf.check_updates" 400 | bl_label = "Check Plugin Updates" 401 | 402 | def execute(self, context): 403 | current_version = sys.modules.get('IntuitionRF').bl_info['version'] 404 | 405 | import requests 406 | import json 407 | r = requests.get("https://api.github.com/repos/Juleinn/IntuitionRF/releases/latest") 408 | 409 | if r.status_code != 200: 410 | self.report({'ERROR'}, "Failed to get latest version number. Please check manually") 411 | return {"FINISHED"} 412 | 413 | latest_tagname = json.loads(r.content)['tag_name'] 414 | 415 | latest_major = int(latest_tagname[0]) 416 | latest_minor = int(latest_tagname[2]) 417 | latest_patch = int(latest_tagname[4]) 418 | 419 | if latest_major == current_version[0] and latest_minor == current_version[1] and latest_patch == current_version[2]: 420 | self.report({'INFO'}, "IntuitionRF is up to date") 421 | else: 422 | self.report({'INFO'}, f"Current {current_version[0]}.{current_version[1]}.{current_version[2]} - Latest: {latest_tagname[:5]} ({latest_tagname})") 423 | 424 | return {"FINISHED"} 425 | 426 | class IntuitionRF_impedance_plotter(bpy.types.Operator): 427 | """Base class for operators for plotting from the object context 428 | aswell as from the scene panel""" 429 | def plot_impedance(self, port, context): 430 | f0 = context.scene.center_freq * 1e6 431 | fc = context.scene.cutoff_freq * 1e6 432 | f = np.linspace(f0-fc,f0+fc,601) 433 | 434 | try: 435 | port.CalcPort(context.scene.intuitionRF_simdir, f) 436 | except: 437 | self.report({'INFO'}, "Failed to calc port") 438 | 439 | Zin = port.uf_tot / port.if_tot 440 | 441 | plt.plot(f/1e6, np.real(Zin), 'k-', label='$\Re\{Z_{in}\}$') 442 | plt.plot(f/1e6, np.imag(Zin), 'r--', label='$\Im\{Z_{in}\}$') 443 | plt.plot(f/1e6, np.absolute(Zin), label='Mag') 444 | plt.legend() 445 | plt.title('Port impedance') 446 | plt.ylabel('Impedance (ohm)') 447 | plt.xlabel('Frequency (MHz)') 448 | plt.grid() 449 | plt.show() 450 | 451 | 452 | class IntuitionRF_OT_plot_port_impedance(IntuitionRF_impedance_plotter): 453 | """Run the currently defined simulation in OpenEMS""" 454 | bl_idname = "intuitionrf.plot_port_impedance" 455 | bl_label = "Plot Impedance" 456 | 457 | def execute(self, context): 458 | active_port = bpy.context.view_layer.objects.active 459 | if not active_port.intuitionRF_properties.object_type == "port": 460 | self.report({'INFO'}, "Cannot plot impedance : not a port") 461 | 462 | port = ports[active_port.name] 463 | 464 | self.plot_impedance(port, context) 465 | 466 | return {"FINISHED"} 467 | 468 | class IntuitionRF_OT_plot_impedance(IntuitionRF_impedance_plotter): 469 | """Run the currently defined simulation in OpenEMS""" 470 | bl_idname = "intuitionrf.plot_impedance" 471 | bl_label = "Plot Impedance" 472 | 473 | def execute(self, context): 474 | active_port = ports[context.scene.intuitionRF_port_selector] 475 | 476 | self.plot_impedance(active_port, context) 477 | 478 | 479 | return {"FINISHED"} 480 | 481 | 482 | def calc_port(port, context): 483 | f0 = context.scene.center_freq * 1e6 484 | fc = context.scene.cutoff_freq * 1e6 485 | f = np.linspace(f0-fc,f0+fc,601) 486 | port.CalcPort(context.scene.intuitionRF_simdir, f) 487 | 488 | Zin = port.uf_tot / port.if_tot 489 | s11 = port.uf_ref/port.uf_inc 490 | s11_dB = 20.0*np.log10(np.abs(s11)) 491 | return f, s11_dB 492 | 493 | class IntuitionRF_returnloss_plotter(bpy.types.Operator): 494 | """Baseclass for running the s11 plots from the object 495 | and scene panels contexts""" 496 | 497 | def plot_s11(self, port, context): 498 | try: 499 | f, s11_dB = calc_port(port, context) 500 | plt.plot(f/1e6, s11_dB) 501 | plt.ylabel('s11 (dB)') 502 | plt.xlabel('f (MHz)') 503 | plt.grid() 504 | plt.show() 505 | 506 | # put resonnant frequency to the scene res. freq 507 | # multiple port not handled yet 508 | res_freq = f[np.argmin(s11_dB)] / 1e6 509 | context.scene.intuitionRF_resonnant_freq = res_freq 510 | except: 511 | self.report({'INFO'}, "Failed to calc port") 512 | 513 | class IntuitionRF_OT_convert_volume_single_frame(bpy.types.Operator): 514 | """Convert the current frame's vtk dump for selected dump object 515 | to OpenVDB file (if frame in available files range, ordered by name)""" 516 | bl_idname = "intuitionrf.convert_volume_single_frame" 517 | bl_label = "Convert Current Frame" 518 | 519 | def execute(self, context): 520 | # find all vtk files in simdir for current dump box (could be 0 if sim never ran) 521 | simdir = context.scene.intuitionRF_simdir 522 | object_name = context.active_object.name 523 | 524 | files = sorted(glob.glob(f"{os.path.join(simdir, object_name)}*vtr")) 525 | frame_relative = context.scene.frame_current - context.scene.frame_start 526 | if frame_relative >= len(files): 527 | self.report({"ERROR"}, "Current frame is out of existing computed dump files bounds") 528 | return {"FINISHED"} 529 | 530 | # find the appropriate file 531 | file_vtr = files[frame_relative] 532 | file_vdb = file_vtr.replace(".vtr", ".vdb") # this will cause bug if the object has .vtr as part of name 533 | self.report({"INFO"}, f"Computing OpenVDB for file {file_vtr}") 534 | 535 | dicing_factor = context.active_object.intuitionRF_properties.dicing_factor 536 | 537 | scale_factor, offset = convert.vtr_to_vdb(file_vtr, file_vdb, dicing_factor) 538 | 539 | # scaling here doesnt seem to work 540 | bpy.ops.object.volume_import( 541 | filepath=file_vdb, 542 | directory=os.path.dirname(file_vdb), 543 | files=[{"name":os.path.basename(file_vdb)}], 544 | relative_path=True, 545 | align='WORLD', 546 | location=(0, 0, 0), 547 | scale=(1,1,1) 548 | ) 549 | 550 | bpy.ops.transform.resize(value=(1/scale_factor, 1/scale_factor, 1/scale_factor)) 551 | bpy.ops.transform.translate(value=offset) 552 | 553 | return {"FINISHED"} 554 | 555 | 556 | class IntuitionRF_OT_convert_volume_all_frames(bpy.types.Operator): 557 | """Convert all frames' vtk dump for selected dump object 558 | to OpenVDB files (for all frames available in file range, ordered by name)""" 559 | bl_idname = "intuitionrf.convert_volume_all_frames" 560 | bl_label = "Convert all Frames" 561 | 562 | def execute(self, context): 563 | simdir = context.scene.intuitionRF_simdir 564 | object_name = context.active_object.name 565 | thread_count = context.active_object.intuitionRF_properties.thread_count 566 | 567 | files = sorted(glob.glob(f"{os.path.join(simdir, object_name)}*vtr")) 568 | files_splits = np.array_split(np.array(list(enumerate(files))), thread_count) 569 | 570 | dicing_factor = context.active_object.intuitionRF_properties.dicing_factor 571 | 572 | args = list(zip(files_splits, [object_name] * len(files_splits), [dicing_factor] * len(files_splits))) 573 | 574 | results = run_parrallel(args, thread_count) 575 | 576 | scale_factor, offset = results[0] # should all be the same 577 | print(scale_factor) 578 | print(offset) 579 | 580 | files = sorted(glob.glob(f"{os.path.join(simdir, object_name)}*vdb")) 581 | files_vdb = [] 582 | for file in files: 583 | file_vdb = os.path.basename(file) 584 | files_vdb.append({"name":file_vdb}) 585 | 586 | print(files_vdb) 587 | file0 = files[0] 588 | 589 | bpy.ops.object.volume_import(filepath=file0, 590 | directory=os.path.dirname(file0), 591 | files=files_vdb, 592 | relative_path=False, 593 | align='WORLD', 594 | location=(0, 0, 0), 595 | scale=(1, 1, 1)) 596 | 597 | bpy.ops.transform.resize(value=(1/scale_factor, 1/scale_factor, 1/scale_factor)) 598 | bpy.ops.transform.translate(value=offset) 599 | 600 | return {"FINISHED"} 601 | 602 | class IntuitionRF_OT_plot_port_return_loss(IntuitionRF_returnloss_plotter): 603 | """Run the currently defined simulation in OpenEMS""" 604 | bl_idname = "intuitionrf.plot_port_return_loss" 605 | bl_label = "Plot s11(dB)" 606 | 607 | def execute(self, context): 608 | active_port = bpy.context.view_layer.objects.active 609 | if not active_port.intuitionRF_properties.object_type == "port": 610 | self.report({'INFO'}, "Cannot plot impedance : not a port") 611 | 612 | port = ports[active_port.name] 613 | 614 | self.plot_s11(port, context) 615 | 616 | return {"FINISHED"} 617 | 618 | class IntuitionRF_OT_plot_return_loss(IntuitionRF_returnloss_plotter): 619 | """Run the currently defined simulation in OpenEMS""" 620 | bl_idname = "intuitionrf.plot_return_loss" 621 | bl_label = "Plot s11(dB)" 622 | 623 | def execute(self, context): 624 | active_port = ports[context.scene.intuitionRF_port_selector] 625 | 626 | self.plot_s11(active_port, context) 627 | 628 | return {"FINISHED"} 629 | 630 | 631 | def PEC_dump_to_scene(filename, context): 632 | if context.scene.intuitionRF_PEC_dump is not None: 633 | bpy.data.objects.remove(bpy.context.scene.intuitionRF_PEC_dump, do_unlink=True) 634 | coords, indices = extract_lines_from_vtp(filename) 635 | 636 | mesh = bpy.data.meshes.new("PEC_dump") # add a new mesh 637 | PEC_dump = bpy.data.objects.new("PEC_dump", mesh) # add a new object using the mesh 638 | bpy.context.collection.objects.link(PEC_dump) 639 | bpy.context.view_layer.objects.active = PEC_dump 640 | 641 | unit = context.scene.intuitionRF_unit 642 | coords = [(coord[0]/unit, coord[1]/unit, coord[2]/unit) for coord in coords] 643 | 644 | mesh.from_pydata(coords, indices, []) 645 | PEC_dump.show_in_front = True 646 | 647 | context.scene.intuitionRF_PEC_dump = PEC_dump 648 | 649 | # Function to read the VTP file and extract line data 650 | # this function was entirely generated by LLM 651 | def extract_lines_from_vtp(filename): 652 | # Create a reader for the VTP file 653 | reader = vtk.vtkXMLPolyDataReader() 654 | reader.SetFileName(filename) 655 | reader.Update() 656 | 657 | # Get the polydata from the reader 658 | polydata = reader.GetOutput() 659 | 660 | # Check if the polydata contains lines 661 | if polydata.GetNumberOfLines() == 0: 662 | print("No lines found in the VTP file.") 663 | return 664 | 665 | # Extract points and lines 666 | points = polydata.GetPoints() 667 | lines = polydata.GetLines() 668 | 669 | # Get the number of points 670 | num_points = points.GetNumberOfPoints() 671 | 672 | # Get the number of lines 673 | num_lines = lines.GetNumberOfCells() 674 | 675 | # Retrieve point coordinates 676 | point_coords = [] 677 | for i in range(num_points): 678 | point_coords.append(points.GetPoint(i)) 679 | 680 | # Retrieve line connectivity 681 | lines.InitTraversal() 682 | id_list = vtk.vtkIdList() 683 | line_connectivity = [] 684 | for i in range(num_lines): 685 | lines.GetNextCell(id_list) 686 | line = [] 687 | for j in range(id_list.GetNumberOfIds()): 688 | line.append(id_list.GetId(j)) 689 | line_connectivity.append(line) 690 | 691 | return point_coords, line_connectivity 692 | 693 | def start_stop_from_BB(bound_box): 694 | min_x = min(round(vert[0], 5) for vert in bound_box) 695 | min_y = min(round(vert[1], 5) for vert in bound_box) 696 | min_z = min(round(vert[2], 5) for vert in bound_box) 697 | max_x = max(round(vert[0], 5) for vert in bound_box) 698 | max_y = max(round(vert[1], 5) for vert in bound_box) 699 | max_z = max(round(vert[2], 5) for vert in bound_box) 700 | 701 | return [min_x, min_y, min_z], [max_x, max_y, max_z] 702 | 703 | def get_axis(verts): 704 | """Determine the normal of verts located inside and axis aligned plane""" 705 | v0 = verts[0] 706 | x = True 707 | y = True 708 | z = True 709 | 710 | margin = .0001 711 | 712 | for vert in verts[1:]: 713 | if abs(vert[0] - v0[0]) > margin: 714 | x = False 715 | if abs(vert[1] - v0[1]) > margin: 716 | y = False 717 | if abs(vert[2] - v0[2]) > margin: 718 | z = False 719 | 720 | if x: 721 | #transpose/slice without numpy 722 | a0 = [v[1] for v in verts] 723 | a1 = [v[2] for v in verts] 724 | return 'x', v0[0], [a0, a1] 725 | elif y: 726 | a0 = [v[0] for v in verts] 727 | a1 = [v[2] for v in verts] 728 | return 'y', v0[1], [a1, a0] 729 | elif z: 730 | a0 = [v[0] for v in verts] 731 | a1 = [v[1] for v in verts] 732 | return 'z', v0[2], [a0, a1] 733 | else: 734 | return 'None', None, None 735 | 736 | 737 | def objects_from_scene(FDTD, CSX, context): 738 | """Exports relevant objects into the continous structure """ 739 | objects_collection = context.scene.intuitionRF_objects.objects 740 | 741 | for o in objects_collection: 742 | if o.intuitionRF_properties.object_type == "metal_aa_faces": 743 | polygons = o.data.polygons 744 | vertices = o.data.vertices 745 | 746 | for index, polygon in enumerate(polygons): 747 | # add a CSX polygon each 748 | # improvement: use single polygon for convex continuous same-normal polygon sets 749 | local_verts = [vertices[polygon.vertices[i]].co for i in range(len(polygon.vertices))] 750 | co = [[round(i[0], 5), 751 | round(i[1], 5), 752 | round(i[2], 5)] for i in local_verts] 753 | normal, elevation, points = get_axis(co) 754 | if normal != "None": 755 | metal = CSX.AddMetal(f"{o.name}_{index}") 756 | prim = metal.AddPolygon(points, normal, elevation) 757 | prim.SetPriority(10) 758 | dirs = 'xyz'.replace(normal, '') 759 | mesh_res = context.scene.intuitionRF_smooth_mesh 760 | #FDTD.AddEdges2Grid(dirs=dirs, properties=metal, metal_edge_res=mesh_res/2) 761 | 762 | # export metals (volume) 763 | if o.intuitionRF_properties.object_type == "metal_volume": 764 | filename = f"{context.scene.intuitionRF_simdir}/{o.name}.stl" 765 | bpy.ops.object.select_all(action='DESELECT') 766 | o.select_set(True) 767 | 768 | unit = context.scene.intuitionRF_unit 769 | # fix for blender 4.2 770 | #bpy.ops.export_mesh.stl(filepath=filename, ascii=True, use_selection=True) 771 | bpy.ops.wm.stl_export(filepath=filename, ascii_format=True, export_selected_objects=True) 772 | 773 | # immediately reimport mesh as a CSX metal part 774 | metal = CSX.AddMetal(o.name) 775 | # import STL file 776 | # 777 | start, stop = start_stop_from_BB(o.bound_box) 778 | #metal.AddBox(start, stop) 779 | reader = metal.AddPolyhedronReader(filename) 780 | reader.SetPriority(10) 781 | reader.SetFileType(1) # 1 STL, 2 PLY 782 | reader.ReadFile() 783 | reader.Update() 784 | reader.SetPrimitiveUsed(True) 785 | 786 | if o.intuitionRF_properties.object_type == "metal_edges": 787 | edges = o.data.edges 788 | vertices = o.data.vertices 789 | 790 | metal = CSX.AddMetal(o.name) 791 | 792 | for index, edge in enumerate(edges): 793 | # TODO rounding 794 | v0 = vertices[edge.vertices[0]].co 795 | v1 = vertices[edge.vertices[1]].co 796 | 797 | coords = np.array([v0, v1]) 798 | metal.AddCurve(coords.T) 799 | 800 | if o.intuitionRF_properties.object_type == "dumpbox": 801 | start, stop = start_stop_from_BB(o.bound_box) 802 | # TODO make this cacheable 803 | 804 | filename_prefix = f"{context.scene.intuitionRF_simdir}/{o.name}" 805 | # remove any previously existing files 806 | for f in glob.glob(f"{filename_prefix}*"): 807 | os.remove(f) 808 | 809 | dumpbox = CSX.AddDump(filename_prefix) 810 | dumpbox.SetDumpType(int(o.intuitionRF_properties.dump_type)) 811 | dumpbox.SetDumpMode(int(o.intuitionRF_properties.dump_mode)) 812 | dumpbox.AddBox(start, stop) 813 | 814 | if o.intuitionRF_properties.object_type == "material": 815 | filename = f"{context.scene.intuitionRF_simdir}/{o.name}.stl" 816 | bpy.ops.object.select_all(action='DESELECT') 817 | o.select_set(True) 818 | #bpy.ops.export_mesh.stl(filepath=filename, ascii=True, use_selection=True) 819 | bpy.ops.wm.stl_export(filepath=filename, ascii_format=True, export_selected_objects=True) 820 | 821 | # immediately reimport mesh as a CSX metal part 822 | if o.intuitionRF_properties.material_use_kappa: 823 | material = CSX.AddMaterial( 824 | o.name, 825 | epsilon = o.intuitionRF_properties.material_epsilon, 826 | kappa = o.intuitionRF_properties.material_kappa 827 | ) 828 | else: 829 | material = CSX.AddMaterial( 830 | o.name, 831 | epsilon = o.intuitionRF_properties.material_epsilon, 832 | ) 833 | # import STL file 834 | # 835 | start, stop = start_stop_from_BB(o.bound_box) 836 | #metal.AddBox(start, stop) 837 | reader = material.AddPolyhedronReader(filename) 838 | reader.SetFileType(1) # 1 STL, 2 PLY 839 | reader.ReadFile() 840 | reader.Update() 841 | reader.SetPrimitiveUsed(True) 842 | 843 | # might night to change name later to account for making modifiers used in the setup 844 | # (not currently the case) 845 | if o.intuitionRF_properties.object_type == "geometry_node": 846 | depsgraph = context.evaluated_depsgraph_get() 847 | evaluated_obj = o.evaluated_get(depsgraph) 848 | 849 | # now we look for vertices flagged with known attributes 850 | attributes = evaluated_obj.data.attributes 851 | 852 | for attribute in attributes: 853 | if attribute.name == "intuitionrf.port_index": 854 | ports_from_geometry_nodes(evaluated_obj, FDTD, CSX) 855 | 856 | if attribute.name == "intuitionrf.pec_edge": 857 | pec_edges_from_geometry_nodes(evaluated_obj, FDTD, CSX) 858 | 859 | if attribute.name == "intuitionrf.pec_aa_face": 860 | pec_aa_faces_from_geometry_nodes(evaluated_obj, FDTD, CSX) 861 | 862 | if attribute.name == "intuitionrf.pec_volume": 863 | pec_volume_from_geometry_nodes(evaluated_obj, context, FDTD, CSX) 864 | 865 | if attribute.name == "intuitionrf.epsilonr": 866 | material_from_geometry_nodes(evaluated_obj, context, FDTD, CSX) 867 | 868 | # needed to add ports after every other element 869 | # TODO handle ports defined in geometry nodes 870 | for o in objects_collection: 871 | if o.intuitionRF_properties.object_type == "port": 872 | start, stop = start_stop_from_BB(o.bound_box) 873 | impedance = o.intuitionRF_properties.port_impedance 874 | port_number = o.intuitionRF_properties.port_number 875 | direction = o.intuitionRF_properties.port_direction 876 | excite = 1.0 if o.intuitionRF_properties.port_active else 0.0 877 | port = FDTD.AddLumpedPort(port_number, impedance, 878 | start, stop, direction, excite) 879 | 880 | ports[o.name] = port 881 | 882 | return FDTD, CSX 883 | 884 | def material_from_geometry_nodes(evaluated_obj, context, FDTD, CSX): 885 | pass 886 | 887 | # sort materials according to their use of kappa, epsilon and (optional) kappa values 888 | # assume the presence of 'use_kappa' and 'kappa' if 'epsilonR' present 889 | material_data = zip(evaluated_obj.data.polygons, 890 | evaluated_obj.data.attributes['intuitionrf.epsilonr'].data, 891 | evaluated_obj.data.attributes['intuitionrf.use_kappa'].data, 892 | evaluated_obj.data.attributes['intuitionrf.kappa'].data 893 | ) 894 | # filter out 0-epsilonR materials (epsilonR = 0 isn't possible and is use to mark faces as not part of a material) 895 | material_data = [item for item in material_data if item[1].value != 0] 896 | 897 | materials = defaultdict(lambda: []) 898 | for index, item in enumerate(material_data): 899 | # we will have a tuple as the key here because we don't want to nest the 900 | # dictionnary output 901 | materials[(item[1].value, item[2].value, item[3].value)].append(item[0]) 902 | 903 | # now for each material we found we export the STL 904 | # and reimport it into OpenEMS immediately 905 | # TODO de-duplicate this whole export STL section 906 | for index, (key, polygons) in enumerate(materials.items()): 907 | epsilonR = key[0] 908 | use_kappa = key[1] 909 | kappa = key[2] 910 | 911 | mesh = bpy.data.meshes.new(f"{evaluated_obj.name}.material.{index}.tmp") 912 | obj = bpy.data.objects.new(f"{evaluated_obj.name}.material.{index}.tmp", mesh) 913 | 914 | # add all vertices from the source object, just not all the faces 915 | # this will litter the tmp object with potentially unused vertices 916 | # but they will be lost on stl export (terrible solution, but it works) 917 | # which avoids rewriting all face vertices indices 918 | # In this case rounding vertex coords is not curcially important because 919 | # it is meant to be evaluated as a mesh anyway 920 | vertices = [item.co for item in evaluated_obj.data.vertices] 921 | faces = [] 922 | 923 | for polygon in polygons: 924 | faces.append(polygon.vertices) 925 | 926 | mesh.from_pydata(vertices, [], faces) 927 | mesh.update() 928 | 929 | context.collection.objects.link(obj) 930 | 931 | filename = f"{context.scene.intuitionRF_simdir}/{evaluated_obj.name}.material.{index}.stl" 932 | bpy.ops.object.select_all(action='DESELECT') 933 | obj.select_set(True) 934 | 935 | #bpy.ops.export_mesh.stl(filepath=filename, ascii=True, use_selection=True) 936 | bpy.ops.wm.stl_export(filepath=filename, ascii_format=True, export_selected_objects=True) 937 | 938 | if use_kappa: 939 | material = CSX.AddMaterial( 940 | f"{evaluated_obj.name}.material.{index}", 941 | epsilon = epsilonR, 942 | kappa = kappa, 943 | ) 944 | else: 945 | material = CSX.AddMaterial( 946 | f"{evaluated_obj.name}.material.{index}", 947 | epsilon = epsilonR, 948 | ) 949 | 950 | reader = material.AddPolyhedronReader(filename) 951 | reader.SetFileType(1) # 1 STL, 2 PLY 952 | reader.ReadFile() 953 | reader.Update() 954 | reader.SetPrimitiveUsed(True) 955 | 956 | bpy.data.objects.remove(obj, do_unlink=True) 957 | 958 | 959 | def pec_volume_from_geometry_nodes(evaluated_obj, context, FDTD, CSX): 960 | # need to 961 | # - create a new object, 962 | # - populate it with the data, 963 | # - export stl, 964 | # - delete object and 965 | # - reimport object 966 | 967 | # now extract the vertices and faces if interest and then put them into the new object 968 | # there is probably a better way to achieve this 969 | pec_data = zip(evaluated_obj.data.polygons, evaluated_obj.data.attributes['intuitionrf.pec_volume'].data) 970 | 971 | # filter out faces of interest 972 | pec_data = [item for item in pec_data if item[1].value == True] 973 | if len(pec_data) == 0: 974 | return 975 | 976 | mesh = bpy.data.meshes.new(f"{evaluated_obj.name}.pec_volume.tmp") 977 | obj = bpy.data.objects.new(f"{evaluated_obj.name}.pec_volume.tmp", mesh) 978 | 979 | # add all vertices from the source object, just not all the faces 980 | # this will litter the tmp object with potentially unused vertices 981 | # but they will be lost on stl export (terrible solution, but it works) 982 | # which avoids rewriting all face vertices indices 983 | # In this case rounding vertex coords is not curcially important because 984 | # it is meant to be evaluated as a mesh anyway 985 | vertices = [item.co for item in evaluated_obj.data.vertices] 986 | faces = [] 987 | for index, item in enumerate(pec_data): 988 | polygon = item[0].vertices 989 | 990 | faces.append(polygon) 991 | 992 | mesh.from_pydata(vertices, [], faces) 993 | mesh.update() 994 | 995 | context.collection.objects.link(obj) 996 | 997 | filename = f"{context.scene.intuitionRF_simdir}/{evaluated_obj.name}.metal_volume.stl" 998 | bpy.ops.object.select_all(action='DESELECT') 999 | obj.select_set(True) 1000 | 1001 | #bpy.ops.export_mesh.stl(filepath=filename, ascii=True, use_selection=True) 1002 | bpy.ops.wm.stl_export(filepath=filename, ascii_format=True, export_selected_objects=True) 1003 | 1004 | # immediately reimport mesh as a CSX metal part 1005 | metal = CSX.AddMetal(f"{evaluated_obj.name}.pec_volume") 1006 | 1007 | reader = metal.AddPolyhedronReader(filename) 1008 | reader.SetPriority(10) 1009 | reader.SetFileType(1) # 1 STL, 2 PLY 1010 | reader.ReadFile() 1011 | reader.Update() 1012 | reader.SetPrimitiveUsed(True) 1013 | 1014 | bpy.data.objects.remove(obj, do_unlink=True) 1015 | 1016 | def pec_aa_faces_from_geometry_nodes(evaluated_obj, FDTD, CSX): 1017 | pec_data = zip(evaluated_obj.data.polygons, evaluated_obj.data.attributes['intuitionrf.pec_aa_face'].data) 1018 | 1019 | # filter out the faces we need 1020 | pec_data = [item for item in pec_data if item[1].value == True] 1021 | if len(pec_data) == 0: 1022 | return 1023 | 1024 | for index, item in enumerate(pec_data): 1025 | # extract vertices from PEC face 1026 | 1027 | # TODO de-duplicate this code from the objects aa face from destructive 1028 | # topology 1029 | polygon = item[0] 1030 | vertices = evaluated_obj.data.vertices 1031 | local_verts = [vertices[polygon.vertices[i]].co for i in range(len(polygon.vertices))] 1032 | co = [[round(i[0], 5), 1033 | round(i[1], 5), 1034 | round(i[2], 5)] for i in local_verts] 1035 | normal, elevation, points = get_axis(co) 1036 | 1037 | if normal != "None": 1038 | metal = CSX.AddMetal(f"{evaluated_obj.name}.pec_aa_face.{index}") 1039 | prim = metal.AddPolygon(points, normal, elevation) 1040 | prim.SetPriority(10) 1041 | 1042 | def pec_edges_from_geometry_nodes(evaluated_obj, FDTD, CSX): 1043 | pec_data = zip(evaluated_obj.data.edges, evaluated_obj.data.attributes['intuitionrf.pec_edge'].data) 1044 | 1045 | 1046 | # filter out any list with no pec edges at all 1047 | # such as not to add empty curve primitives to the SIM 1048 | pec_data = [item for item in pec_data if item[1].value == True] 1049 | if len(pec_data) == 0: 1050 | return 1051 | 1052 | metal = CSX.AddMetal(f"{evaluated_obj.name}.edges") 1053 | 1054 | for item in pec_data: 1055 | 1056 | v0 = evaluated_obj.data.vertices[item[0].vertices[0]].co 1057 | v1 = evaluated_obj.data.vertices[item[0].vertices[1]].co 1058 | 1059 | # round coords 1060 | v0[0] = round(v0[0], 5) 1061 | v0[1] = round(v0[1], 5) 1062 | v0[2] = round(v0[2], 5) 1063 | v1[0] = round(v1[0], 5) 1064 | v1[1] = round(v1[1], 5) 1065 | v1[2] = round(v1[2], 5) 1066 | coords = np.array([v0, v1]) 1067 | metal.AddCurve(coords.T) 1068 | 1069 | 1070 | def ports_from_geometry_nodes(evaluated_obj, FDTD, CSX): 1071 | # assume other required attributes are defined aswell 1072 | # (user didn't add store named attribute node of name 'intuitionrf.port_index') 1073 | 1074 | port_data = zip(evaluated_obj.data.vertices, 1075 | evaluated_obj.data.attributes['intuitionrf.port_index'].data, 1076 | evaluated_obj.data.attributes['intuitionrf.port_impedance'].data, 1077 | evaluated_obj.data.attributes['intuitionrf.port_axis'].data, 1078 | evaluated_obj.data.attributes['intuitionrf.port_active'].data 1079 | ) 1080 | 1081 | port_dict = defaultdict(lambda: []) # map each port data by the port index 1082 | # ports[index] = [(vertex, impedance, axis, active), (vertex, impedance, axis, active), ...] 1083 | for item in port_data: 1084 | if item[1].value == 0: # invalid port index 1085 | continue 1086 | # we want to extract the actual float values from the nested data structures here for 1087 | # further processing 1088 | port_dict[item[1].value].append(list(tuple( 1089 | ( 1090 | # x,y,z coords of the point 1091 | round(item[0].co[0], 5), 1092 | round(item[0].co[1], 5), 1093 | round(item[0].co[2], 5), 1094 | item[2].value, 1095 | # x, y, z coords of the orientation at the point 1096 | round(item[3].vector[0], 5), 1097 | round(item[3].vector[1], 5), 1098 | round(item[3].vector[2], 5), 1099 | item[4].value 1100 | ) 1101 | ))) 1102 | 1103 | # now we find for each port the average orienation vector, average active value, etc.. 1104 | for key, value in port_dict.items(): 1105 | value = np.array(value) 1106 | min_x, min_y, min_z = (np.min(value[:,0]), np.min(value[:,1]), np.min(value[:,2])) 1107 | max_x, max_y, max_z = (np.max(value[:,0]), np.max(value[:,1]), np.max(value[:,2])) 1108 | axis_x, axis_y, axis_z = (np.mean(value[:,4]), np.mean(value[:,5]), np.mean(value[:,6])) 1109 | axis_x, axis_y, axis_z = (abs(axis_x), abs(axis_y), abs(axis_z)) 1110 | axis = "x" 1111 | if axis_y > axis_x and axis_y > axis_z: 1112 | axis = 'y' 1113 | if axis_z > axis_x and axis_z > axis_y: 1114 | axis = 'z' 1115 | 1116 | impedance = np.mean(value[:,3]) 1117 | active = float(np.mean(value[:,7]) > 0) 1118 | 1119 | port = FDTD.AddLumpedPort(key, impedance, 1120 | [min_x, min_y, min_z], [max_x, max_y, max_z], axis, active) 1121 | 1122 | # register it in the port list for later processing 1123 | ports[str(key)] = port 1124 | 1125 | def meshlines_from_scene(CSX, context): 1126 | lines = context.scene.intuitionRF_lines 1127 | x, y, z = extract_lines_xyz(lines) 1128 | 1129 | mesh = CSX.GetGrid() 1130 | unit = context.scene.intuitionRF_unit 1131 | mesh.SetDeltaUnit(unit) 1132 | 1133 | # put lines in CSXCAD 1134 | mesh.AddLine('x', list(x)) 1135 | mesh.AddLine('y', list(y)) 1136 | mesh.AddLine('z', list(z)) 1137 | 1138 | # also extract lines from objects 1139 | CSX = meshlines_from_vertex_groups(CSX, context) 1140 | 1141 | # smooth as required by user 1142 | if context.scene.intuitionRF_smooth_mesh: 1143 | # smooth all directions the same 1144 | # per grid granularty should be determiner by fixed lines 1145 | # (easy to place graphically) 1146 | mesh.SmoothMeshLines('x', context.scene.intuitionRF_smooth_max_res, context.scene.intuitionRF_smooth_ratio) 1147 | mesh.SmoothMeshLines('y', context.scene.intuitionRF_smooth_max_res, context.scene.intuitionRF_smooth_ratio) 1148 | mesh.SmoothMeshLines('z', context.scene.intuitionRF_smooth_max_res, context.scene.intuitionRF_smooth_ratio) 1149 | pass 1150 | return CSX 1151 | 1152 | def meshlines_from_vertex_groups(CSX, context): 1153 | # extract list of coordinates from vertex group named 'intuitionRF_verts' 1154 | x = set() 1155 | y = set() 1156 | z = set() 1157 | 1158 | objects_collection = context.scene.intuitionRF_objects.objects 1159 | for o in objects_collection: 1160 | # dump box need no meshing an neither do "None" 1161 | if o.intuitionRF_properties.object_type == "dumpbox": 1162 | continue 1163 | if o.intuitionRF_properties.object_type == "none": 1164 | continue 1165 | # skip objects that have no anchors assigned 1166 | if "intuitionRF_anchors" not in o.vertex_groups: 1167 | continue 1168 | 1169 | # need to iterate over all vertices and check their weight in the group 1170 | intuitionRF_vgroup = o.vertex_groups['intuitionRF_anchors'].index 1171 | verts = o.data.vertices 1172 | for v in verts: 1173 | weights = [group.weight for group in v.groups if group.group == intuitionRF_vgroup] 1174 | # check in group, any nonzero weight will do 1175 | if len(weights) == 1 and weights[0] != 0: 1176 | 1177 | x.add(v.co[0]) 1178 | y.add(v.co[1]) 1179 | z.add(v.co[2]) 1180 | 1181 | # TODO loop the collection only once 1182 | for o in objects_collection: 1183 | # we also want to extract vertices from named attributes stored in geometry nodes 1184 | # TODO merge this with the above as to apply modifiers everywhere 1185 | dg = context.evaluated_depsgraph_get() 1186 | evaluated_object = o.evaluated_get(dg) 1187 | attr_anchors = [attr for attr in evaluated_object.data.attributes if attr.name == "intuitionrf.anchor"] 1188 | if len(attr_anchors) == 1: 1189 | attr_anchors = attr_anchors[0] 1190 | for i, v in enumerate(evaluated_object.data.vertices): 1191 | if attr_anchors.data[i].value == True: 1192 | 1193 | x.add(round(v.co[0], 5)) 1194 | y.add(round(v.co[1], 5)) 1195 | z.add(round(v.co[2], 5)) 1196 | 1197 | mesh = CSX.GetGrid() 1198 | # put lines in CSXCAD 1199 | if len(list(x)) > 0: 1200 | mesh.AddLine('x', list(sorted(x))) 1201 | mesh.AddLine('y', list(sorted(y))) 1202 | mesh.AddLine('z', list(sorted(z))) 1203 | 1204 | return CSX 1205 | 1206 | class IntuitionRF_OT_add_preview_lines(bpy.types.Operator): 1207 | """ Add openEMS meshing lines preview """ 1208 | bl_idname = "intuitionrf.add_preview_lines" 1209 | bl_label = "Add meshing lines preview" 1210 | 1211 | def execute(self, context): 1212 | if context.scene.intuitionRF_previewlines is not None: 1213 | bpy.data.objects.remove(bpy.context.scene.intuitionRF_previewlines, do_unlink=True) 1214 | 1215 | FDTD = openEMS(NrTS=1, EndCriteria=1e-4) 1216 | 1217 | CSX = CSXCAD.ContinuousStructure() 1218 | CSX = meshlines_from_scene(CSX, context) 1219 | FDTD.SetCSX(CSX) 1220 | FDTD.SetGaussExcite( context.scene.center_freq * 1e6, context.scene.cutoff_freq * 1e6) 1221 | FDTD.SetBoundaryCond( ['MUR', 'MUR', 'MUR', 'MUR', 'MUR', 'MUR'] ) 1222 | 1223 | FDTD, CSX = objects_from_scene(FDTD, CSX, context) 1224 | mesh = CSX.GetGrid() 1225 | 1226 | # retrieve lines 1227 | x = mesh.GetLines('x') 1228 | y = mesh.GetLines('y') 1229 | z = mesh.GetLines('z') 1230 | 1231 | unit = context.scene.intuitionRF_unit 1232 | 1233 | # create a new mesh 1234 | mesh = bpy.data.meshes.new("preview_lines") # add a new mesh 1235 | preview_lines = bpy.data.objects.new("preview_lines", mesh) # add a new object using the mesh 1236 | bpy.context.collection.objects.link(preview_lines) 1237 | # bpy.context.view_layer.objects.active = preview_lines 1238 | 1239 | # draw lines in each directions 1240 | verts = [] 1241 | edges = [] 1242 | faces = [] 1243 | for item_x in x: 1244 | # draw lines at min_y from min_z to mzx_z 1245 | verts.append(tuple((item_x, min(y), min(z)))) 1246 | verts.append(tuple((item_x, min(y), max(z)))) 1247 | # add the latest two vertices to a new edge 1248 | edges.append([len(verts) - 1, len(verts) - 2]) 1249 | # draw bottom lines 1250 | verts.append(tuple((item_x, min(y), min(z)))) 1251 | verts.append(tuple((item_x, max(y), min(z)))) 1252 | edges.append([len(verts) - 1, len(verts) - 2]) 1253 | 1254 | for item_y in y: 1255 | verts.append(tuple((min(x), item_y, min(z)))) 1256 | verts.append(tuple((min(x), item_y, max(z)))) 1257 | edges.append([len(verts) - 1, len(verts) - 2]) 1258 | verts.append(tuple((min(x), item_y, min(z)))) 1259 | verts.append(tuple((max(x), item_y, min(z)))) 1260 | edges.append([len(verts) - 1, len(verts) - 2]) 1261 | 1262 | for item_z in z: 1263 | verts.append(tuple((min(x), min(y), item_z))) 1264 | verts.append(tuple((max(x), min(y), item_z))) 1265 | edges.append([len(verts) - 1, len(verts) - 2]) 1266 | verts.append(tuple((min(x), min(y), item_z))) 1267 | verts.append(tuple((min(x), max(y), item_z))) 1268 | edges.append([len(verts) - 1, len(verts) - 2]) 1269 | 1270 | mesh.from_pydata(verts, edges, faces) 1271 | mesh.validate(verbose=True) 1272 | preview_lines.show_name = True 1273 | preview_lines.hide_select = True 1274 | context.scene.intuitionRF_previewlines = preview_lines 1275 | return {"FINISHED"} 1276 | 1277 | def register(): 1278 | bpy.utils.register_class(IntuitionRF_OT_add_meshline_x) 1279 | bpy.utils.register_class(IntuitionRF_OT_add_meshline_y) 1280 | bpy.utils.register_class(IntuitionRF_OT_add_meshline_z) 1281 | 1282 | bpy.utils.register_class(IntuitionRF_OT_add_domain) 1283 | bpy.utils.register_class(IntuitionRF_OT_add_wavelength_cube) 1284 | bpy.utils.register_class(IntuitionRF_OT_add_default_lines) 1285 | bpy.utils.register_class(IntuitionRF_OT_add_preview_lines) 1286 | 1287 | bpy.utils.register_class(IntuitionRF_OT_preview_CSX) 1288 | bpy.utils.register_class(IntuitionRF_OT_preview_PEC_dump) 1289 | bpy.utils.register_class(IntuitionRF_OT_run_sim) 1290 | bpy.utils.register_class(IntuitionRF_OT_plot_port_return_loss) 1291 | bpy.utils.register_class(IntuitionRF_OT_plot_return_loss) 1292 | bpy.utils.register_class(IntuitionRF_OT_plot_port_impedance) 1293 | bpy.utils.register_class(IntuitionRF_OT_plot_impedance) 1294 | bpy.utils.register_class(IntuitionRF_OT_compute_NF2FF) 1295 | bpy.utils.register_class(IntuitionRF_OT_check_updates) 1296 | 1297 | bpy.utils.register_class(IntuitionRF_OT_convert_volume_single_frame) 1298 | bpy.utils.register_class(IntuitionRF_OT_convert_volume_all_frames) 1299 | 1300 | def unregister(): 1301 | bpy.utils.unregister_class(IntuitionRF_OT_add_meshline_x) 1302 | bpy.utils.unregister_class(IntuitionRF_OT_add_meshline_y) 1303 | bpy.utils.unregister_class(IntuitionRF_OT_add_meshline_z) 1304 | 1305 | bpy.utils.unregister_class(IntuitionRF_OT_add_domain) 1306 | bpy.utils.unregister_class(IntuitionRF_OT_add_default_lines) 1307 | bpy.utils.unregister_class(IntuitionRF_OT_add_preview_lines) 1308 | bpy.utils.unregister_class(IntuitionRF_OT_add_wavelength_cube) 1309 | 1310 | bpy.utils.unregister_class(IntuitionRF_OT_preview_CSX) 1311 | bpy.utils.unregister_class(IntuitionRF_OT_preview_PEC_dump) 1312 | bpy.utils.unregister_class(IntuitionRF_OT_run_sim) 1313 | bpy.utils.unregister_class(IntuitionRF_OT_plot_port_return_loss) 1314 | bpy.utils.unregister_class(IntuitionRF_OT_plot_return_loss) 1315 | bpy.utils.unregister_class(IntuitionRF_OT_plot_port_impedance) 1316 | bpy.utils.unregister_class(IntuitionRF_OT_plot_impedance) 1317 | bpy.utils.unregister_class(IntuitionRF_OT_compute_NF2FF) 1318 | bpy.utils.unregister_class(IntuitionRF_OT_check_updates) 1319 | -------------------------------------------------------------------------------- /panels/objects.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import sys 3 | import bmesh 4 | import mathutils 5 | from mathutils import geometry 6 | 7 | from CSXCAD import CSXCAD 8 | from openEMS import openEMS, ports 9 | from openEMS.physical_constants import * 10 | import multiprocessing 11 | 12 | import os 13 | sys.path.append(os.path.abspath('..')) 14 | from .. operators import meshing 15 | 16 | class IntuitionRF_ObjectProperties(bpy.types.PropertyGroup): 17 | object_type: bpy.props.EnumProperty( 18 | name = 'Type', 19 | description = 'Select an option', 20 | items = [ 21 | ('none', 'None', 'Ignored for computations'), 22 | ('metal_volume', 'metal (Volume)', 'metal (Volume)'), 23 | ('metal_aa_faces', 'metal (AA faces)', 'metal (AA faces)'), 24 | ('metal_edges', 'metal (edges)', 'metal (edges)'), 25 | ('material', 'material (\u03B5,\u03BA)', 'material defined by \u03B5 and \u03BA'), 26 | ('dumpbox', 'Dump Box', 'Dump box for E or H fields (to be specified)'), 27 | ('nf2ff', 'NF2FF Box', 'Near Field to Far Field computation box'), 28 | ('port', 'Port', 'Excitation Port'), 29 | ('geometry_node', 'Geometry Node', 'Defer primitive generation to custom geometry nodes') 30 | ] 31 | ) 32 | # material properties 33 | material_epsilon: bpy.props.FloatProperty(name='\u03B5', default=4.6) 34 | material_use_kappa: bpy.props.BoolProperty(name='Use \u03BA', default=False) 35 | material_kappa: bpy.props.FloatProperty(name='\u03BA', default=2000) 36 | 37 | # port properties 38 | port_number: bpy.props.IntProperty(name='Port Number', default=1) 39 | port_impedance: bpy.props.FloatProperty(name='Impedance (ohms)', default=50) 40 | port_direction: bpy.props.EnumProperty( 41 | name = 'Direction', 42 | description = 'Port Excitation Direction', 43 | items = [ 44 | ('x', 'x', 'x'), 45 | ('y', 'y', 'y'), 46 | ('z', 'z', 'z') 47 | ] 48 | ) 49 | port_active: bpy.props.BoolProperty(name='Active', default=False) 50 | 51 | dump_type: bpy.props.EnumProperty( 52 | name = 'Dump Type', 53 | description = 'Dump Type', 54 | items = [ 55 | ("0", "E field/time", "E-field time-domain dump (default)"), 56 | ("1", "H field/time", "H-field time-domain dump"), 57 | ("2", "Current/time", "electric current time-domain dump"), 58 | ("3", "Current density/time", "total current density (rot(H)) time-domain dump"), 59 | ("10", "E field/freq", " E-field frequency-domain dump"), 60 | ("11", "H field/freq", " H-field frequency-domain dump"), 61 | ("12", "Current/freq", " electric current frequency-domain dump"), 62 | ("13", "Current density/freq", " total current density (rot(H)) frequency-domain dump"), 63 | ("20", "local SAR/freq", " local SAR frequency-domain dump"), 64 | ("21", "1g avg. SAR/freq", " 1g averaging SAR frequency-domain dump"), 65 | ("22", "10g avg. SAR/freq", " 10g averaging SAR frequency-domain dump"), 66 | ("29", "raw SAR", " raw data needed for SAR calculations (electric field FD, cell volume, conductivity and density)") 67 | ] 68 | ) 69 | 70 | dump_mode: bpy.props.EnumProperty( 71 | name = 'Dump Mode', 72 | description = 'Dump Mode', 73 | items = [ 74 | ("0", "no-interpolation", "no-interpolation"), 75 | ("1", "node-interpolation", "node-interpolation"), 76 | ("2", "cell-interpolation", "cell-interpolation") 77 | ] 78 | ) 79 | 80 | dicing_factor: bpy.props.IntProperty( 81 | name = 'Dicing factor', 82 | description = 'Dicing factor as a multiple of the computed cell size (smallest irregular grid cell size)', 83 | default = 8, 84 | ) 85 | 86 | use_log: bpy.props.BoolProperty(name = 'use_log', default=True) 87 | 88 | thread_count: bpy.props.IntProperty( 89 | name = 'thread count', 90 | description = 'Number of thread for multi-frame compute', 91 | default = multiprocessing.cpu_count() - 1, 92 | ) 93 | 94 | 95 | # object tab properties panel 96 | class OBJECT_PT_intuitionRFPanel(bpy.types.Panel): 97 | bl_label = "IntuitionRF" 98 | bl_idname = "OBJECT_PT_intuitionRFPanel" 99 | bl_space_type = 'PROPERTIES' 100 | bl_region_type = 'WINDOW' 101 | bl_context = "object" 102 | 103 | @classmethod 104 | def poll(cls, context): 105 | return context.object is not None 106 | 107 | def draw(self, context): 108 | layout = self.layout 109 | obj = context.object 110 | 111 | # Display the custom properties in the panel 112 | layout.prop(obj.intuitionRF_properties, "object_type") 113 | 114 | layout.separator() 115 | 116 | if obj.intuitionRF_properties.object_type == 'material': 117 | row = layout.row() 118 | row.prop(obj.intuitionRF_properties, "material_epsilon") 119 | row = layout.row() 120 | row.prop(obj.intuitionRF_properties, "material_use_kappa") 121 | if obj.intuitionRF_properties.material_use_kappa: 122 | row.prop(obj.intuitionRF_properties, "material_kappa") 123 | if obj.intuitionRF_properties.object_type == 'port': 124 | row = layout.row() 125 | row.prop(obj.intuitionRF_properties, "port_number") 126 | row.prop(obj.intuitionRF_properties, "port_active") 127 | row = layout.row() 128 | row.prop(obj.intuitionRF_properties, "port_impedance") 129 | row = layout.row() 130 | row.prop(obj.intuitionRF_properties, "port_direction") 131 | if obj.name in meshing.ports.keys(): 132 | row = layout.row() 133 | row.operator("intuitionrf.plot_port_return_loss") 134 | row.operator("intuitionrf.plot_port_impedance") 135 | 136 | if obj.intuitionRF_properties.object_type == "dumpbox": 137 | row = layout.row() 138 | row.prop(obj.intuitionRF_properties, "dump_type") 139 | row = layout.row() 140 | row.prop(obj.intuitionRF_properties, "dump_mode") 141 | row = layout.row() 142 | box = row.box() 143 | row = box.row() 144 | row.label(text="Convert to Blender volume") 145 | row = box.row() 146 | row.prop(obj.intuitionRF_properties, "dicing_factor") 147 | row = box.row() 148 | row.operator("intuitionrf.convert_volume_single_frame") 149 | row = box.row() 150 | row = box.row() 151 | row.prop(obj.intuitionRF_properties, "thread_count") 152 | row = box.row() 153 | row.operator("intuitionrf.convert_volume_all_frames") 154 | 155 | def register(): 156 | # register object classes 157 | bpy.utils.register_class(OBJECT_PT_intuitionRFPanel) 158 | bpy.utils.register_class(IntuitionRF_ObjectProperties) 159 | bpy.types.Object.intuitionRF_properties = bpy.props.PointerProperty(type=IntuitionRF_ObjectProperties) 160 | 161 | def unregister(): 162 | # unregister object classes 163 | bpy.utils.unregister_class(OBJECT_PT_intuitionRFPanel) 164 | bpy.utils.unregister_class(IntuitionRF_ObjectProperties) 165 | -------------------------------------------------------------------------------- /panels/scene.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import bpy 3 | import sys 4 | import bmesh 5 | from bpy.props import EnumProperty 6 | import mathutils 7 | from mathutils import geometry 8 | 9 | from CSXCAD import CSXCAD 10 | from numpy import minimum 11 | from openEMS import openEMS 12 | from openEMS.physical_constants import * 13 | 14 | class IntuitionRFPanel(bpy.types.Panel): 15 | """Creates a Panel in the scene context of the properties editor""" 16 | bl_label = "IntuitionRF" 17 | bl_idname = "SCENE_PT_layout" 18 | bl_space_type = 'PROPERTIES' 19 | bl_region_type = 'WINDOW' 20 | bl_context = "scene" 21 | 22 | def draw(self, context): 23 | layout = self.layout 24 | scene = context.scene 25 | 26 | row = layout.row() 27 | row.prop(scene, "intuitionRF_unit") 28 | 29 | # excitation parameters 30 | box = layout.box() 31 | box.label(text="Excitation") 32 | row = box.row() 33 | row.prop(scene, "intuitionRF_excitation_type") 34 | row = box.row() 35 | row.prop(scene, "center_freq") 36 | if context.scene.intuitionRF_excitation_type == "gauss": 37 | row = box.row() 38 | row.prop(scene, "cutoff_freq") 39 | 40 | if context.scene.intuitionRF_excitation_type == "custom": 41 | row = box.row() 42 | row.prop(scene, "cutoff_freq") 43 | row = box.row() 44 | row.prop(scene, "intuitionRF_excitation_custom_function") 45 | 46 | # collection to be used 47 | box = layout.box() 48 | row = box.row() 49 | row.prop(scene, "intuitionRF_objects") 50 | 51 | # meshing helpers 52 | box = layout.box() 53 | row = box.row() 54 | wavelength = 300.0 / context.scene.center_freq 55 | row.label(text = f"\u03BB = {wavelength:.2}m" ) 56 | row.operator("intuitionrf.add_wavelength_cube") 57 | row = box.row() 58 | row.prop(scene, "intuitionRF_lines") 59 | row = box.row() 60 | row.operator("intuitionrf.add_default_lines") 61 | row = box.row() 62 | row.prop(scene, "intuitionRF_smooth_mesh") 63 | row = box.row() 64 | row.prop(scene, "intuitionRF_smooth_ratio") 65 | row = box.row() 66 | row.prop(scene, "intuitionRF_smooth_max_res") 67 | row = box.row() 68 | row.operator("intuitionrf.add_preview_lines") 69 | row = box.row() 70 | row.operator("intuitionrf.add_meshline_x") 71 | row.operator("intuitionrf.add_meshline_y") 72 | row.operator("intuitionrf.add_meshline_z") 73 | 74 | # sim preview 75 | box = layout.box() 76 | row = box.row() 77 | row.prop(scene, 'intuitionRF_simdir') 78 | row = box.row() 79 | row.operator("intuitionrf.preview_csx") 80 | row.operator("intuitionrf.preview_pec_dump") 81 | 82 | # run SIM and far field compute 83 | box = layout.box() 84 | row = box.row() 85 | row.prop(scene, "intuitionRF_oversampling") 86 | row = box.row() 87 | row.operator("intuitionrf.run_sim") 88 | box = layout.box() 89 | row = box.row() 90 | row.prop(scene, 'intuitionRF_resonnant_freq') 91 | row = box.row() 92 | row.operator("intuitionrf.compute_nf2ff") 93 | 94 | # list ports and compute parameters 95 | box = layout.box() 96 | row = box.row() 97 | row.label(text='Port Index') 98 | row.prop(scene, "intuitionRF_port_selector") 99 | row = box.row() 100 | row.operator("intuitionrf.plot_impedance") 101 | row.operator("intuitionrf.plot_return_loss") 102 | 103 | # check releases 104 | box = layout.box() 105 | row = box.row() 106 | row.label(text="Checking for latest release on Github.") 107 | row = box.row() 108 | row.label(text="This will require internet access.") 109 | row = box.row() 110 | row.label(text="This will not auto-install anything.") 111 | row = box.row() 112 | row.operator("intuitionrf.check_updates") 113 | 114 | def update_port_list(ports_list): 115 | ports = [] 116 | for index, port in ports_list.items(): 117 | ports.append(tuple((index, str(index), str(index)))) 118 | 119 | bpy.types.Scene.intuitionRF_port_selector = EnumProperty( 120 | name = '', 121 | description='Select a port for s11 plotting', 122 | items = ports 123 | ) 124 | 125 | def register(): 126 | bpy.utils.register_class(IntuitionRFPanel) 127 | bpy.types.Scene.intuitionRF_unit = bpy.props.FloatProperty( 128 | name='Unit (scale)', 129 | description = 130 | """Blender to OpenEMS scaling factor. 131 | 1e-3 means 1 blender unit (meter) 132 | is 1mm in simulation""", 133 | default=1 134 | ) 135 | bpy.types.Scene.center_freq = bpy.props.FloatProperty(name='Center Freq (Mhz)', default=868.00) 136 | bpy.types.Scene.cutoff_freq = bpy.props.FloatProperty(name='Cutoff Freq (Mhz)', default=2*868.00) 137 | bpy.types.Scene.intuitionRF_objects = bpy.props.PointerProperty(type=bpy.types.Collection) 138 | bpy.types.Scene.intuitionRF_domain = bpy.props.PointerProperty(type=bpy.types.Object, name='Domain') 139 | bpy.types.Scene.intuitionRF_excitation_type = bpy.props.EnumProperty( 140 | name = '', 141 | description = 'Select an option', 142 | items = [ 143 | ('gauss', 'Gaussian', 'Gaussian Excite'), 144 | ('sine', 'Sine', 'Sine Excite'), 145 | ('custom', 'Custom', 'Custom Excite') 146 | ] 147 | ) 148 | bpy.types.Scene.intuitionRF_excitation_custom_function = bpy.props.StringProperty(name='Custom excitation function') 149 | 150 | bpy.types.Scene.intuitionRF_lines = bpy.props.PointerProperty(type=bpy.types.Object, name='lines') 151 | bpy.types.Scene.intuitionRF_smooth_mesh = bpy.props.BoolProperty( 152 | name="Smooth mesh lines", 153 | description="Smooth mesh lines", 154 | default = True 155 | ) 156 | bpy.types.Scene.intuitionRF_previewlines = bpy.props.PointerProperty(type=bpy.types.Object, name='preview_lines') 157 | bpy.types.Scene.intuitionRF_smooth_max_res = bpy.props.FloatProperty(name='Smooth max resolution', default=3) 158 | bpy.types.Scene.intuitionRF_smooth_ratio = bpy.props.FloatProperty(name='Smooth ratio', default=1.4) 159 | bpy.types.Scene.intuitionRF_PEC_dump = bpy.props.PointerProperty(type=bpy.types.Object, name='lines') 160 | 161 | tmpdir = tempfile.mkdtemp() 162 | 163 | bpy.types.Scene.intuitionRF_simdir = bpy.props.StringProperty( 164 | name="Directory", 165 | description="Simulation Directory", 166 | default=tmpdir, 167 | maxlen=1024, 168 | subtype='DIR_PATH' 169 | ) 170 | 171 | bpy.types.Scene.intuitionRF_resonnant_freq = bpy.props.FloatProperty( 172 | name = 'Resonnant frequency (MHz)', 173 | default = 0 174 | ) 175 | 176 | bpy.types.Scene.intuitionRF_oversampling = bpy.props.IntProperty( 177 | name = 'oversampling', 178 | description = 'Oversampling of probes/dumps as a multiple of the nyquist rate', 179 | default=1, 180 | min=1 181 | ) 182 | 183 | bpy.types.Scene.intuitionRF_port_selector = bpy.props.EnumProperty( 184 | name = 'port', 185 | description = 'Select port for parameter plotting', 186 | items = [ 187 | ] 188 | ) 189 | 190 | def unregister(): 191 | bpy.utils.unregister_class(IntuitionRFPanel) 192 | 193 | del bpy.types.Scene.center_freq 194 | del bpy.types.Scene.cutoff_freq 195 | del bpy.types.Scene.intuitionRF_objects 196 | del bpy.types.Scene.intuitionRF_domain 197 | del bpy.types.Scene.intuitionRF_excitation_type 198 | del bpy.types.Scene.intuitionRF_unit 199 | del bpy.types.Scene.intuitionRF_lines 200 | del bpy.types.Scene.intuitionRF_previewlines 201 | del bpy.types.Scene.intuitionRF_smooth_max_res 202 | del bpy.types.Scene.intuitionRF_smooth_ratio 203 | del bpy.types.Scene.intuitionRF_PEC_dump 204 | del bpy.types.Scene.intuitionRF_simdir 205 | --------------------------------------------------------------------------------