Подбор оптимальной геометрии в компас 3d с помощью fluid x3d

Начало

Всё началось с того, что мне подкинули интересный проект с разработкой тороидальных винтов.

Фото для примера одного из такого винта

Фото для примера одного из такого винта

Всё началось в solidworks с того, что был смоделирован стандартный двухлопастной винт с изменяемыми углами атаки через переменные.

Непосредственно вот он

Непосредственно вот он

Далее начался подбор параметров для запуска исследования:

Для начала использовали периодичную расчетную область, так как где-то вычитали то, что для продувки винтов необходимо использовать именно её, но через неделю активного недоумевания, почему при изменениях расчетной области менялась тяга, мы поняли, как работает периодическая расчетная область

Примерно так мы это поняли только потоки идут во все стороны

Примерно так мы это поняли только потоки идут во все стороны

То есть потоки расчетной области копируются в соседние ячейки с такой же периодичностью, как и область расчета и да всё это делается во вращении локальной области Averaging — это где вращается модель в сетке, в остальном всё такие же настройки, как и во всех гайдах. Также было непонимание почему 5 дюймовый винт на 25 000 об/мин создаёт тягу в 0,001Н.

Затем просто в научных интересах во вращении локальной области поставил Sliding — в нем сетка вращается вокруг модели, то есть берется цилиндр сетки и начинает вращаться в сетке. И, о чудо,  были получены правдоподобные результаты.

На следующем этапе стало интересно, что будет, если покрутить разрешение сетки, ведь от него время «Продувки » варьируется от пары секунд до недели не на самом слабом ПК с intel 9 10 940 и quadro a5000, для этого был отсканирован винт сканером shining 3d ue pro. Так, к слову, у него погрешность сканирования 0,01 мм и произведен реверс инжиниринг для оптимизации геометрии. В качестве знака окончания продувки смотрели график тяги от итераций и останавливали «продувку», когда тяга выходила на «полку».

отсканированный винт с моделированием

отсканированный винт с моделированием

юГрафик зависимости тяги от разрешения сетки

юГрафик зависимости тяги от разрешения сетки

Тяга винта на испытательном стенде была 6,75Н и тут появились вопросы к solidworks, к этому прибавляется, что было сказано всё это делать на отечественном софте, из‑за этого пересели на компас, но в нем встроенный модуль cfd расчета не позволяет «продувать» подвижные детали. Было принято решение использовать open source проект fluid x3d, который в отличие от solidworks производит расчет на видеокарте вместо процессора, что сокращает время расчета с недели до нескольких минут! Итак, после первых тестов fluid были написаны следующие настройки для продувки винта и экспорта значений продувки в txt файл.

void main_setup() { 
	// ################################################################## define simulation box size, viscosity and volume force ###################################################################
	const uint memory = 3500u; // available VRAM of GPU(s) in MB
	const float lbm_u = 0.12f;
	const float box_scale = 6.0f;
	const float si_u = 0.17f;
	const float si_nu = 0.17f, si_rho = 1.0f;
	const float si_width = 0.3f, si_height = 0.3f, si_length = 0.3f;
	const float si_A = si_width * si_height + 2.0f * 0.05f * 0.03f;
	const float si_T = 0.25f;
	const float si_Lx = units.x(box_scale * si_width);
	const float si_Ly = units.x(box_scale * si_length);
	const float si_Lz = units.x(0.5f * (box_scale - 1.0f) * si_width + si_height);
	const uint3 lbm_N = resolution(float3(3.0f, 3.0f, 1.0f), memory); // input: simulation box aspect ratio and VRAM occupation in MB, output: grid resolution
	units.set_m_kg_s((float)lbm_N.y, lbm_u, 1.0f, box_scale * si_length, si_u, si_rho);
	print_info("Re = " + to_string(to_uint(units.si_Re(si_width, si_u, si_nu))));
	const float lbm_nu = units.nu(si_nu);
	const float si_omega = 1000u;
	const float lbm_omega = units.omega(si_omega);
	const float lbm_length = units.x(si_length);
	LBM lbm(lbm_N, lbm_nu);
	const uint lbm_T = 5000u;
	const uint lbm_dt = 10u;
	const float summa_cd = 0;
	float cd[10];
	float avergate_cd[10];
	int a = 0;
	float avergate = 0;
	int i;

	const float radius = 0.25f * (float)lbm_N.x;
	const float3 center = float3(lbm.center().x, lbm.center().y, 0.36f * radius);
	// ###################################################################################### define geometry ######################################################################################
	Mesh* mesh = read_stl(get_exe_path() + "../stl/33.stl", lbm.size(), lbm.center(), float3x3(float3(0, 0, 1), radians(90.0f)), lbm_length);
	mesh->translate(float3(0.0f, units.x(0.5f * (0.5f * box_scale * si_length - si_width)) - mesh->pmin.y, 1.0f - mesh->pmin.z));
	lbm.voxelize_mesh_on_device(mesh, TYPE_S | TYPE_X); // https://github.com/nathanrooy/ahmed-bluff-body-cfd/blob/master/geometry/ahmed_25deg_m.stl converted to binary
	const uint Nx = lbm.get_Nx(), Ny = lbm.get_Ny(), Nz = lbm.get_Nz(); parallel_for(lbm.get_N(), [&](ulong n) { uint x = 0u, y = 0u, z = 0u; lbm.coordinates(n, x, y, z);
	//if(z==0u) lbm.flags[n] = TYPE_S;
	//if(lbm.flags[n]!=TYPE_S) lbm.u.y[n] = lbm_u;
	//if(x==0u||x==Nx-1u||y==0u||y==Ny-1u||z==Nz-1u) lbm.flags[n] = TYPE_E;
		}); // ####################################################################### run simulation, export images and data ##########################################################################
	//lbm.graphics.visualization_modes = VIS_FLAG_SURFACE | VIS_Q_CRITERION;
	//lbm.graphics.set_camera_centered(20.0f, 30.0f, 0.0f, 1.648722f);
	lbm.run(0u); // initialize simulation
	
#if defined(FP16S)
	const string path = get_exe_path() + "FP16S/" + to_string(memory) + "MB/";
#elif defined(FP16C)
	const string path = get_exe_path() + "FP16C/" + to_string(memory) + "MB/";
#else // FP32
	const string path = get_exe_path() + "FP32/" + to_string(memory) + "MB/";
#endif // FP32
	//lbm.write_status(path);
	//write_file(path+"Cd.dat", "# t\tCd\n");
	//std::ofstream app;          // поток для записи
	//app.open("force.txt");      // открываем файл для записи
	while (true) { // main simulation loop
		while (true) { // main simulation loop
			//if (lbm.graphics.next_frame(units.t(si_T), 5.0f)) {
				Clock clock;
				lbm.run(lbm_dt);
				lbm.calculate_force_on_boundaries();
				lbm.F.read_from_device();
				lbm.voxelize_mesh_on_device(mesh, TYPE_S, center, float3(0.0f), float3(0.0f, 0.0f, lbm_omega));
				const float3 lbm_force = lbm.calculate_force_on_object(TYPE_S | TYPE_X);
				const float Cd = units.si_F(lbm_force.y); // expect Cd to be too large by a factor 1.3-2.0x; need wall model
				const float moment = units.si_F(lbm_force.x);
				
				cd[a] = Cd;
				a = a +1;
				if (a == 10) {
					int a = 0;
				}
				for (i = 0; i < 10; i++) {
					avergate = (avergate + cd[i]) / 10;
				}
				avergate_cd[a] = avergate;
				println("\r" + to_string(Cd, 3u) + " " + to_string(moment, 3u) + "                                                                               ");
				//		write_line(path+"Cd.dat", to_string(lbm.get_t())+"\t"+to_string(Cd, 3u)+"\n");
				mesh->rotate(float3x3(float3(0, 0, 1), lbm_omega * (float)lbm_dt));// rotate mesh
				lbm.update_fields();
				lbm.run(lbm_dt);
				std::ofstream ate;
				ate.open("force.txt", std::ios_base::app);
				ate << (to_string(Cd,3u) + " " + to_string(moment,3u)) << std::endl;
				ate.close();
			
				//if (a > 3) {
				//	if (avergate_cd[a] - avergate_cd[a - 3] < 0.1 && avergate_cd[a] - avergate_cd[a - 3] > -0.1) {
				//		break;
				//	}
				//}
				
				

#if defined(GRAPHICS) && !defined(INTERACTIVE_GRAPHICS)
				//		lbm.graphics.write_frame(path+"images/");
#endif // GRAPHICS && !INTERACTIVE_GRAPHICS
			}
			lbm.run(1u);
			//app.close();
		}
		//lbm.write_status(path);
	} /**/
//}

https://github.com/ProjectPhysX/FluidX3D — ссылка на fluid кому интересно будет поковыряйтесь советую

Теперь нам надо импортировать данные продувки в python ибо не на чем другом я программировать не умею

import os
import subprocess



def run():
    command = ["путь к fluid.exe","/c","runas",'/user:administrator',"regedit"]
    p = subprocess.Popen(command, stdin = subprocess.PIPE)
    #p.stdin.write("1961")
    p.communicate()
    
    
def get_result():
    data = open("путь к force.txt","r")
    a = data.read()
    a = a[:-1]
    l = a.split("\n")
    force = 0
    moment = 0
    #print(l)
    for i in range(len(l)):
        l[i] = l[i].split(" ")
        force += abs(float(l[i][0]))
        moment += abs(float(l[i][1]))
    force = round(force/len(l),2)
    moment = round(moment/len(l),2)
    a = [force,moment]
    return a



def remove_dat():
    os.remove("путь к force.txt")

Теперь к компасу. Там есть два варианта. Начнем с того, что попроще.

Рисуем стандартный двухлопастной винт по сечениям, причем в эскизах делаем углы, которые выносим в глобальные переменные.

Получаем что-то такое

Получаем что-то такое

Теперь пора заняться api компаса

# -*- coding: utf-8 -*-
#|1

import pythoncom
from win32com.client import Dispatch, gencache
import KompasAPI7
import LDefin3D
import MiscellaneousHelpers as MH
import os
def connect(): 
    global iPart,iDocument3D,kompas_document
#  Подключим константы API Компас
    kompas6_constants = gencache.EnsureModule("{75C9F5D0-B5B8-4526-8681-9903C567D2ED}", 0, 1, 0).constants
    kompas6_constants_3d = gencache.EnsureModule("{2CAF168C-7961-4B90-9DA2-701419BEEFE3}", 0, 1, 0).constants

#  Подключим описание интерфейсов API5
    kompas6_api5_module = gencache.EnsureModule("{0422828C-F174-495E-AC5D-D31014DBBE87}", 0, 1, 0)
    kompas_object = kompas6_api5_module.KompasObject(Dispatch("Kompas.Application.5")._oleobj_.QueryInterface(kompas6_api5_module.KompasObject.CLSID, pythoncom.IID_IDispatch))
    MH.iKompasObject  = kompas_object

#  Подключим описание интерфейсов API7
    kompas_api7_module = gencache.EnsureModule("{69AC2981-37C0-4379-84FD-5DD2F3C0A520}", 0, 1, 0)
    application = kompas_api7_module.IApplication(Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(kompas_api7_module.IApplication.CLSID, pythoncom.IID_IDispatch))
    MH.iApplication  = application


    Documents = application.Documents
#  Получим активный документ
    kompas_document = application.ActiveDocument
#kompas_document_3d = kompas_api7_module.IKompasDocument3D(kompas_document)
    iDocument3D = kompas_object.ActiveDocument3D()
    iPart = iDocument3D.GetPart(LDefin3D.pTop_Part)
    return iPart,iDocument3D
def rebuild(iPart,iDocument3D):
    iPart.RebuildModel()
    iDocument3D.RebuildDocument()
def export_stl():
    kompas_document.SaveAs("path\\FluidX3D-master\\stl\\33.STL")
def edit_param(iPart,angle1,angle2):
    
    global VariableCollection
    
    VariableCollection = iPart.VariableCollection()
    VariableCollection.GetByName("angle1",True,True).value =  angle1
    VariableCollection.GetByName("angle2",True,True).value = angle2


def remove_stl():
    """
    Remove existing STL file
    """
    # Delete file
    os.remove("path\\FluidX3D-master\\stl\\33.STL")

Тут ничего сложного, просто подключаемся к компасу, меняем две переменные и сохраняем/удаляем stl.

Теперь для полного счастья нужен генетический алгоритм:

import random
import api_kompas
import api_fluid

def create_obj(size,obj):                                               #создаем рандомный объект
    obj = []
    for i in range(size):
        obj.append(random.randint(0,45))
    return obj

def create_population(pop_size,obj_size):                               #создем рандомное поколени
    population = []
    obj = []
    for i in range(pop_size):
        population.append(create_obj(obj_size,obj))
    return population

def calc_obj(iPart,iDocument,obj):                                      #считаем тягу и момент для объекта
    api_kompas.edit_param(iPart,obj[0],obj[1])
    api_kompas.rebuild(iPart,iDocument)
    api_kompas.export_stl()
    api_fluid.run()
    obj.append(api_fluid.get_result()[0])
    obj.append(api_fluid.get_result()[1])
    api_fluid.remove_dat()
    api_kompas.remove_stl()

def calc_pop(iPart,iDocument,population):                               #считаем тягу и момент для популяции
    for i in range(len(population)):
        calc_obj(iPart,iDocument,population[i])

def calc_k_obj(obj):                                                    #считаем коофицент выживаемости для объекта
    obj.append(obj[len(obj)-2]/obj[len(obj)-1])

def calc_k_pop(population):                                             #считаем коофицент выживаемости для поколения
    for i in range(len(population)):
        calc_k_obj(population[i])

def sort_population(population):
    for i in range(len(population)-1):
        for g in range(len(population)-i-1):
            if population[g][len(population[i])-1] < population[g+1][len(population[i])-1]:
                population[g][len(population[i])-1],population[g+1][len(population[i])-1]  = population[g+1][len(population[i])-1],population[g][len(population[i])-1]

def get_max_k(population):
    sort_population(population)
    m = population[0][len(population[0])-1]
    return m

def get_weight(population):                                               #получаем коофиценты выживаемости поколения в отдельный массив
    weight = []
    sort_population(population)
    for i in range(len(population)):
        weight.append(population[i][len(population[0])-1]/get_max_k(population))
    #print(weight)
    return weight



def get_parents_for_new_obj(population):                                    #определяем родителей для объекта следующего поколения
    parents = []
    sort_population(population)
    parents.append(random.choices(population,weights= get_weight(population))[0])
    parent = random.choices(population,weights= get_weight(population))[0]
    while parents[0] == parent:
        parent = random.choices(population,weights= get_weight(population))[0]
    parents.append(parent)
    return parents

def get_parent_for_new_population(population):                              #определяем родителей для всего поколения
    parents_population = []
    for i in range(len(population)):
        parents_population.append(get_parents_for_new_obj(population))
    return parents_population

def select(parents):                                                          #размножаем
    new_obj = [0,0]
    
    new_obj[0] = parents[0][0]
    new_obj[1] = parents[1][1]
    print(new_obj)
    return new_obj

def select_population(parents):                                                 #размножаем поколение
    new_population = []
    for i in range(len(parents)):
        new_population.append(select(parents[i]))
    
    return new_population

def mutate(population):                                                         #мутируем
    for i in range(len(population)//2):
        for g in range(len(population[0])):
            gen = random.randint(0,len(population[0])-1)
            population[i][gen] = random.randint(0,45)

Все функции подписаны так, что должно быть всё понятно. Не спорю, что он не идеален, — всё делалось за ночь до выступления :-)

Итак, теперь сделаем простенький main для генетического алгоритма:

import api_kompas
import api_fluid
import pythoncom
from win32com.client import Dispatch, gencache
import KompasAPI7
import LDefin3D
import MiscellaneousHelpers as MH
import exsel
import time
import gen_alg
#  Подключим константы API Компас
kompas6_constants = gencache.EnsureModule("{75C9F5D0-B5B8-4526-8681-9903C567D2ED}", 0, 1, 0).constants
kompas6_constants_3d = gencache.EnsureModule("{2CAF168C-7961-4B90-9DA2-701419BEEFE3}", 0, 1, 0).constants

#  Подключим описание интерфейсов API5
kompas6_api5_module = gencache.EnsureModule("{0422828C-F174-495E-AC5D-D31014DBBE87}", 0, 1, 0)
kompas_object = kompas6_api5_module.KompasObject(Dispatch("Kompas.Application.5")._oleobj_.QueryInterface(kompas6_api5_module.KompasObject.CLSID, pythoncom.IID_IDispatch))
MH.iKompasObject  = kompas_object

#  Подключим описание интерфейсов API7
kompas_api7_module = gencache.EnsureModule("{69AC2981-37C0-4379-84FD-5DD2F3C0A520}", 0, 1, 0)
application = kompas_api7_module.IApplication(Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(kompas_api7_module.IApplication.CLSID, pythoncom.IID_IDispatch))
MH.iApplication  = application


Documents = application.Documents
#  Получим активный документ
kompas_document = application.ActiveDocument
kompas_document_3d = kompas_api7_module.IKompasDocument3D(kompas_document)
iDocument3D = kompas_object.ActiveDocument3D()
iPart = iDocument3D.GetPart(LDefin3D.pTop_Part)
api_kompas.connect()
a = 0

populations = []
populations.append(gen_alg.create_population(10,2))
for i in range(50):
    parents = []
    gen_alg.calc_pop(iPart,iDocument3D,populations[i])
    gen_alg.calc_k_pop(populations[i])
    parents = gen_alg.get_parent_for_new_population(populations[i])
    print(parents)
    populations.append(gen_alg.select_population(parents))
    
    if i-1 < 50:
        print(populations[i+1])
        gen_alg.mutate(populations[i+1])

До 35 строчки мы просто подключаемся к компасу, дальше создаем список популяций, добавляем первую популяцию, производим с ним продувки, считаем тягу с моментом, дальше коэффициенты выживаемости— подбираем родителей для нового поколения и создаем новое поколение и это всё повторяется несколько раз

Теперь вернёмся к тороидальным винтам.

Концепция будет следующей: в питоне генерируем точки с нужными координатами, перерисовываем в компас, обводим сплайнами, натягиваем поверхности, сшиваем — лопасти готовы.

Предлагаю начать с профилей. Для этого переписываем функцию на пайтон:

def naca4(number, chord=1, angle_of_attack=0, num_points=15, angle_z = 0,x0 =0 ,y0 = 0, z0 = 0, long = 1):
    # Разбиваем число NACA на составляющие
    m = int(number[0]) / 100.0
    p = int(number[1]) / 10.0
    t = int(number[2:]) / 100.0
    
    # Угол атаки в радианах
    alpha = np.radians(angle_of_attack)
    beta = np.radians(angle_z)
    # Создаем массив x от 0 до 1 с num_points точками
    z = np.linspace(0, chord, num_points)
    x =[0]
    for i in range(num_points-2):
        #if i<= 0.5*num_points:
        x.append((1-np.cos(np.pi * z[i]))/2)
        
        #else:
        #    x.append((1+(1-np.cos(np.pi*z[i])))/2)  
    x.append(1)
            
    l = []
    for i in x:
        l.append(round(i, 2))    
    x = np.array(x)
       
    # Толщина профиля
    yt = 5 * t * chord * (
        0.2969 * np.sqrt(x/chord) 
        - 0.1260 * (x/chord) 
        - 0.3516 * (x/chord)**2 
        + 0.2843 * (x/chord)**3 
        - 0.1015 * (x/chord)**4
    )
    
    # Камбер линия
    yc = np.where((x/chord) < p,
                  m * (x/chord) / p**2 * (2 * p - (x/chord)),
                  m * (1 - (x/chord)) / (1 - p)**2 * (1 + (x/chord) - 2 * p))
    
    # Угол наклона камбер линии
    dyc_dx = np.where((x/chord) < p,
                      2 * m / p**2 * (p - (x/chord)),
                      2 * m / (1 - p)**2 * (p - (x/chord)))
    theta = np.arctan(dyc_dx)
    
    # Координаты верхней и нижней поверхностей
    xu = x - yt * np.sin(theta)
    yu = yc + yt * np.cos(theta)

    xl = x + yt * np.sin(theta)
    yl = yc - yt * np.cos(theta)
    return xu,yu,xl,yl

Тут просто генерируется профиль без преобразований и да, в последствии выяснилось, что на концах профиля нужно больше точек поэтому расстояние между точками идет по синусоиде, то есть в начале и конце их получается больше и это плавно переливается. Итак, нам надо добавить перемещение профиля, масштабирование, верчение и так далее. Тут, я думаю, тоже сильно ничего сложного нету просто вставлю полную функцию:

def naca4(number, chord=1, angle_of_attack=0, num_points=15, angle_z = 0,x0 =0 ,y0 = 0, z0 = 0, long = 1):
    # Разбиваем число NACA на составляющие
    m = int(number[0]) / 100.0
    p = int(number[1]) / 10.0
    t = int(number[2:]) / 100.0
    
    # Угол атаки в радианах
    alpha = np.radians(angle_of_attack)
    beta = np.radians(angle_z)
    # Создаем массив x от 0 до 1 с num_points точками
    z = np.linspace(0, chord, num_points)
    x =[0]
    for i in range(num_points-2):
        #if i<= 0.5*num_points:
        x.append((1-np.cos(np.pi * z[i]))/2)
        
        #else:
        #    x.append((1+(1-np.cos(np.pi*z[i])))/2)  
    x.append(1)
            
    l = []
    for i in x:
        l.append(round(i, 2))    
    x = np.array(x)
       
    # Толщина профиля
    yt = 5 * t * chord * (
        0.2969 * np.sqrt(x/chord) 
        - 0.1260 * (x/chord) 
        - 0.3516 * (x/chord)**2 
        + 0.2843 * (x/chord)**3 
        - 0.1015 * (x/chord)**4
    )
    
    # Камбер линия
    yc = np.where((x/chord) < p,
                  m * (x/chord) / p**2 * (2 * p - (x/chord)),
                  m * (1 - (x/chord)) / (1 - p)**2 * (1 + (x/chord) - 2 * p))
    
    # Угол наклона камбер линии
    dyc_dx = np.where((x/chord) < p,
                      2 * m / p**2 * (p - (x/chord)),
                      2 * m / (1 - p)**2 * (p - (x/chord)))
    theta = np.arctan(dyc_dx)
    
    # Координаты верхней и нижней поверхностей
    xu = x - yt * np.sin(theta)
    yu = yc + yt * np.cos(theta)

    xl = x + yt * np.sin(theta)
    yl = yc - yt * np.cos(theta)
    
    # Поворачиваем профиль на угол атаки
    xu_rot = xu * np.cos(alpha) - yu * np.sin(alpha)
    yu_rot = xu * np.sin(alpha) + yu * np.cos(alpha)
    
    xl_rot = xl * np.cos(alpha) - yl * np.sin(alpha)
    yl_rot = xl * np.sin(alpha) + yl * np.cos(alpha)
 

    #создаём 2 массива заполненые 0
    zu_rot = []
    zl_rot = []
    for i in range(len(xu_rot)):
        zu_rot.append(0)
        zl_rot.append(0)
    zu_rot = np.array(zu_rot)
    zl_rot = np.array(zl_rot)
      
    #маштабируем
    xu_rot = xu_rot * long
    xl_rot = xl_rot * long
    yl_rot = yl_rot * long
    yu_rot = yu_rot * long
    zu_rot = zu_rot * long
    zl_rot = zl_rot * long
    x11 = xu_rot[0]
    y11 = yu_rot[0]
    z11 = 10
    x12 = xu_rot[0] + 10
    y12 = yu_rot[0]
    z12 = 0
    x13 = xu_rot[0]
    y13 = yu_rot[0]+10
    z13 = 0
    x21 = xu_rot[-1]
    y21 = yu_rot[-1]
    z21 = 10
    x22 = xu_rot[-1] + 10
    y22 = yu_rot[-1]
    z22 = 0
    x23 = xu_rot[-1]
    y23 = yu_rot[-1]+10
    z23 = 0
    #перемещаем профиль в x0,y0,z0
    xu_rot = xu_rot + x0
    xl_rot = xl_rot + x0
    yl_rot = yl_rot + y0
    yu_rot = yu_rot + y0
    zu_rot = zu_rot + z0
    zl_rot = zl_rot + z0
    x11 = x11 + x0
    y11 = y11 + y0
    z11 = z11 + z0
    x12 = x12 + x0
    y12 = y12 + y0
    z12 = z12 + z0
    x13 = x13 + x0
    y13 = y13 + y0
    z13 = z13 + z0
    x21 = x21 + x0
    y21 = y21 + y0
    z21 = z21 + z0
    x22 = x22 + x0
    y22 = y22 + y0
    z22 = z22 + z0
    x23 = x23 + x0
    y23 = y23 + y0
    z23 = z23 + z0
    print(zl_rot)
    #поворчиваем угол по оси z
    xu_rot_z = xu_rot * np.cos(beta) - zu_rot*np.sin(beta)
    zu_rot_z = xu_rot * np.sin(beta) + zu_rot*np.cos(beta)
    xl_rot_z = xl_rot * np.cos(beta) - zu_rot*np.sin(beta)
    zl_rot_z = xl_rot * np.sin(beta) + zl_rot*np.cos(beta)
    x11_rot = x11* np.cos(beta) - z11*np.sin(beta)
    z11_rot = x11* np.sin(beta) + z11*np.cos(beta)
    x12_rot = x12* np.cos(beta) - z12*np.sin(beta)
    z12_rot = x12* np.sin(beta) + z12*np.cos(beta)
    x13_rot = x13* np.cos(beta) - z13*np.sin(beta)
    z13_rot = x13* np.sin(beta) + z13*np.cos(beta)
    x21_rot = x21* np.cos(beta) - z21*np.sin(beta)
    z21_rot = x21* np.sin(beta) + z21*np.cos(beta)
    x22_rot = x22* np.cos(beta) - z22*np.sin(beta)
    z22_rot = x22* np.sin(beta) + z22*np.cos(beta)
    x23_rot = x23* np.cos(beta) - z23*np.sin(beta)
    z23_rot = x23* np.sin(beta) + z23*np.cos(beta)
    
    #print(zu_rot_z)
     #Отображаем профиль для отладки можно закоментировать
    #plt.figure(figsize=(12, 6))
    #plt.plot(xu_rot, yu_rot, 'b')
    #plt.plot(xl_rot, yl_rot, 'b')
    #plt.grid(True)
    #plt.axis('equal')
    #plt.show()
    return xu_rot_z,xl_rot_z[1:15],yu_rot,yl_rot[1:15],zu_rot_z,zl_rot_z[1:15],x11_rot,y11,z11_rot,x12_rot,y12,z12_rot,x13_rot,y13,z13_rot,x21_rot,y21,z21_rot,x22_rot,y22,z22_rot,x23_rot,y23,z23_rot

Итак, когда у нас профили, нам нужны кромки винта. Тут я сильно ничего не стал выдумывать — просто рисую точки в цилиндрической системе координат. Тут тоже без пояснений.

def point_edge(angle_z = 0, r = 1, angle_xy = 90, height = 1, x0 = 0, z0 = 0):
    alpha  = np.radians(angle_z)
    beta = np.radians(angle_xy)
    z = r
    x = 0
    z = z * np.cos(alpha) - x * np.sin(alpha)
    x = z * np.sin(alpha) + x * np.cos(alpha)
    z = z + z0
    x = x + x0
    #x = x * np.cos(beta) - z * np.sin(beta)
    #z = x * np.sin(beta) + z * np.cos(beta)
    y  = height
    return x,y,z

Теперь нам надо нарисовать это всё в компасе: просто в цикле идем по точкам, читаем их координаты и рисуем:

def create_point_profl(xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,kompas_document_3d):
    kompas6_constants = gencache.EnsureModule("{75C9F5D0-B5B8-4526-8681-9903C567D2ED}", 0, 1, 0).constants
    kompas6_constants_3d = gencache.EnsureModule("{2CAF168C-7961-4B90-9DA2-701419BEEFE3}", 0, 1, 0).constants
    kompas_api7_module = gencache.EnsureModule("{69AC2981-37C0-4379-84FD-5DD2F3C0A520}", 0, 1, 0)
    application = kompas_api7_module.IApplication(Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(kompas_api7_module.IApplication.CLSID, pythoncom.IID_IDispatch))
    MH.iApplication  = application

    for i in range(len(xu_rot)):
        iPart7 = kompas_document_3d.TopPart
        

        iModelContainer = iPart7._oleobj_.QueryInterface(kompas_api7_module.IModelContainer.CLSID, pythoncom.IID_IDispatch)
        iModelContainer = kompas_api7_module.IModelContainer(iModelContainer)
        iPoints3D = iModelContainer.Points3D
        iPoint3D = iPoints3D.Add()
        iPoint3D.ParameterType = kompas6_constants_3d.ksPParamCoord
        iPoint3D.Symbol = kompas6_constants.ksDotPoint
        iPoint3D.X = xu_rot[i]
        iPoint3D.Y = yu_rot[i]
        iPoint3D.Z = zu_rot[i]
        iPoint3D.Update()
    for i in range(len(xl_rot)):
        iPart7 = kompas_document_3d.TopPart
        

        iModelContainer = iPart7._oleobj_.QueryInterface(kompas_api7_module.IModelContainer.CLSID, pythoncom.IID_IDispatch)
        iModelContainer = kompas_api7_module.IModelContainer(iModelContainer)
        iPoints3D = iModelContainer.Points3D
        iPoint3D = iPoints3D.Add()
        iPoint3D.ParameterType = kompas6_constants_3d.ksPParamCoord
        iPoint3D.Symbol = kompas6_constants.ksDotPoint
        iPoint3D.X = xl_rot[i]
        iPoint3D.Y = yl_rot[i]
        iPoint3D.Z = zl_rot[i]
        iPoint3D.Update()
def create_point_edge(x,y,z,kompas_document_3d):
    kompas6_constants = gencache.EnsureModule("{75C9F5D0-B5B8-4526-8681-9903C567D2ED}", 0, 1, 0).constants
    kompas6_constants_3d = gencache.EnsureModule("{2CAF168C-7961-4B90-9DA2-701419BEEFE3}", 0, 1, 0).constants
    kompas_api7_module = gencache.EnsureModule("{69AC2981-37C0-4379-84FD-5DD2F3C0A520}", 0, 1, 0)
    application = kompas_api7_module.IApplication(Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(kompas_api7_module.IApplication.CLSID, pythoncom.IID_IDispatch))
    MH.iApplication  = application

    iPart7 = kompas_document_3d.TopPart
    iModelContainer = iPart7._oleobj_.QueryInterface(kompas_api7_module.IModelContainer.CLSID, pythoncom.IID_IDispatch)
    iModelContainer = kompas_api7_module.IModelContainer(iModelContainer)
    iPoints3D = iModelContainer.Points3D
    iPoint3D = iPoints3D.Add()
    iPoint3D.ParameterType = kompas6_constants_3d.ksPParamCoord
    iPoint3D.Symbol = kompas6_constants.ksDotPoint
    iPoint3D.X = x
    iPoint3D.Y = y
    iPoint3D.Z = z
    iPoint3D.Update()

Теперь нам надо один раз отрисовать их в компасе:

c0cb48a610da252ac30b0c8ad678e03b.png

Нарисовали, соединили сплайнами. Отлично (не очень, я не нашел, как автоматически менять координаты точек, поэтому каждой координате каждой точки присваиваем глобальную переменную и делаем её внешней).

что-то по типу такого, кто мудрый подскажите что можно сделать

что-то по типу такого, кто мудрый подскажите что можно сделать

Итак, теперь функции для изменений переменных. Ничего сложного, просто взято из документации:

def edit_profl(iPart,num,xu,xl,yu,yl,zu,zl):
    global VariableCollection
    VariableCollection = iPart.VariableCollection()
    for i in range(len(xu)):
        VariableCollection.GetByName(f"px{num}1{i+1}",True,True).value = xu[i]
        VariableCollection.GetByName(f"py{num}1{i+1}",True,True).value = yu[i]
        VariableCollection.GetByName(f"pz{num}1{i+1}",True,True).value = zu[i]
    for i in range(len(xl)):
        VariableCollection.GetByName(f"px{num}2{i+1}",True,True).value = xl[i]
        VariableCollection.GetByName(f"py{num}2{i+1}",True,True).value = yl[i]
        VariableCollection.GetByName(f"pz{num}2{i+1}",True,True).value = zl[i]
        print(f"pz{num}2{i+1}")
def edit_cross_points(iPart,num,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23):
    global VariableCollection
    VariableCollection = iPart.VariableCollection()
    VariableCollection.GetByName(f"xx{num}1",True,True).value = x11
    VariableCollection.GetByName(f"xy{num}1",True,True).value = y11
    VariableCollection.GetByName(f"xz{num}1",True,True).value = z11
    VariableCollection.GetByName(f"yx{num}1",True,True).value = x12
    VariableCollection.GetByName(f"yy{num}1",True,True).value = y12
    VariableCollection.GetByName(f"yz{num}1",True,True).value = z12
    VariableCollection.GetByName(f"zx{num}1",True,True).value = x13
    VariableCollection.GetByName(f"zy{num}1",True,True).value = y13
    VariableCollection.GetByName(f"zz{num}1",True,True).value = z13
    VariableCollection.GetByName(f"xx{num}2",True,True).value = x21
    VariableCollection.GetByName(f"xy{num}2",True,True).value = y21
    VariableCollection.GetByName(f"xz{num}2",True,True).value = z21
    VariableCollection.GetByName(f"yx{num}2",True,True).value = x22
    VariableCollection.GetByName(f"yy{num}2",True,True).value = y22
    VariableCollection.GetByName(f"yz{num}2",True,True).value = z22
    VariableCollection.GetByName(f"zx{num}2",True,True).value = x23
    VariableCollection.GetByName(f"zy{num}2",True,True).value = y23
    VariableCollection.GetByName(f"zz{num}2",True,True).value = z23

def edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,edge):
    global VariableCollection
    VariableCollection = iPart.VariableCollection()
    VariableCollection.GetByName(f"{edge}x1",True,True).value = x1
    VariableCollection.GetByName(f"{edge}y1",True,True).value = y1
    VariableCollection.GetByName(f"{edge}z1",True,True).value = z1
    VariableCollection.GetByName(f"{edge}x2",True,True).value = x2
    print(f"{edge}x2")
    VariableCollection.GetByName(f"{edge}y2",True,True).value = y2
    VariableCollection.GetByName(f"{edge}z2",True,True).value = z2

Ну и небольшой main просто для прогонки винта:

import api_kompas
import api_fluid
import pythoncom
from win32com.client import Dispatch, gencache
import KompasAPI7
import LDefin3D
import MiscellaneousHelpers as MH
import exsel
import time
import points
iPart,iDocument = api_kompas.connect()
#xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23 = points.naca4("4812", angle_of_attack=-5,angle_z=-45,x0=-10,y0=0,z0=0,long=20)
#api_kompas.edit_profl(iPart,1,xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot)
#api_kompas.edit_cross_points(iPart,1,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23)
#x1,y1,z1 = points.point_edge(45,20,90,3.75,xu_rot[0],zu_rot[0])
#x2,y2,z2 = points.point_edge(45,30,90,5,xu_rot[0],zu_rot[0])
#api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kdf")
#x1,y1,z1 = points.point_edge(45,20,90,3.75,xu_rot[14],zu_rot[14])
#x2,y2,z2 = points.point_edge(45,30,90,5,xu_rot[14],zu_rot[14])
#api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kdb")
#xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23 = points.naca4("4812", angle_of_attack=-5,angle_z=45,x0=-10,y0=30,z0=0,long=20)
#api_kompas.edit_profl(iPart,2,xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot)
#api_kompas.edit_cross_points(iPart,2,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23)
#x1,y1,z1 = points.point_edge(-45,20,90,26.25,xu_rot[0],zu_rot[0])
#x2,y2,z2 = points.point_edge(-45,30,90,25,xu_rot[0],zu_rot[0])
#api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kuf")
#x1,y1,z1 = points.point_edge(-45,20,90,26.25,xu_rot[14],zu_rot[14])
#x2,y2,z2 = points.point_edge(-45,30,90,25,xu_rot[14],zu_rot[14])
#print(x1,y1,z1,x2,y2,z2)
#xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23 = points.naca4("4812", angle_of_attack=-5,angle_z=0,x0=-10,y0=50,z0=15,long=20)
#api_kompas.edit_profl(iPart,3,xu_rot,xl_rot,zu_rot,zl_rot,yu_rot,yl_rot)
#api_kompas.edit_cross_points(iPart,3,x11,z11,y11,x12,z12,y12,x13,z13,y13,x21,z21,y21,x22,z22,y22,x23,z23,y23)
#api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kub")
#api_kompas.rebuild(iPart,iDocument)
for q in range(0,45,5):
    for w in range(0,45,5):
        for e in range(0,15,3):
            for r in range(0,15,3):
                for t in range(0,45,5):
                    for y in range(0,45,5):
                        for u in range(15,30,3):
                            for i in range(15,30,3):
                                xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23 = points.naca4("4812", angle_of_attack=-q,angle_z=-w,x0=-10,y0=0,z0=0,long=20)
                                api_kompas.edit_profl(iPart,1,xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot)
                                api_kompas.edit_cross_points(iPart,1,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23)
                                x1,y1,z1 = points.point_edge(w,20,90,e,xu_rot[0],zu_rot[0])
                                x2,y2,z2 = points.point_edge(w,30,90,r,xu_rot[0],zu_rot[0])
                                api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kdf")
                                x1,y1,z1 = points.point_edge(w,20,90,e,xu_rot[14],zu_rot[14])
                                x2,y2,z2 = points.point_edge(w,30,90,r,xu_rot[14],zu_rot[14])
                                api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kdb")
                                xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23 = points.naca4("4812", angle_of_attack=-t,angle_z=y,x0=-10,y0=30,z0=0,long=20)
                                api_kompas.edit_profl(iPart,2,xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot)
                                api_kompas.edit_cross_points(iPart,2,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23)
                                x1,y1,z1 = points.point_edge(-y,20,90,u,xu_rot[0],zu_rot[0])
                                x2,y2,z2 = points.point_edge(-y,30,90,i,xu_rot[0],zu_rot[0])
                                api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kuf")
                                x1,y1,z1 = points.point_edge(-y,20,90,u,xu_rot[14],zu_rot[14])
                                x2,y2,z2 = points.point_edge(-y,30,90,i,xu_rot[14],zu_rot[14])
                                xu_rot,xl_rot,yu_rot,yl_rot,zu_rot,zl_rot,x11,y11,z11,x12,y12,z12,x13,y13,z13,x21,y21,z21,x22,y22,z22,x23,y23,z23 = points.naca4("4812", angle_of_attack=-5,angle_z=0,x0=-10,y0=50,z0=15,long=20)
                                api_kompas.edit_profl(iPart,3,xu_rot,xl_rot,zu_rot,zl_rot,yu_rot,yl_rot)
                                api_kompas.edit_cross_points(iPart,3,x11,z11,y11,x12,z12,y12,x13,z13,y13,x21,z21,y21,x22,z22,y22,x23,z23,y23)
                                api_kompas.edit_edge_points(iPart,x1,y1,z1,x2,y2,z2,"kub")
                                api_kompas.rebuild(iPart,iDocument)

После выполнения немного измененного main получил табличку в exsel, оттуда достал самый лучший винт по отношению тяга/момент и напечатал на фотополимере:

скрин из компаса

скрин из компаса

Так выглядит после печати, но это один самых первых - форма не идеальна

Так выглядит после печати, но это один самых первых — форма не идеальна

И так далее, напечатав такой вот винт, я пришел к небольшому удивлению, что fluid ошибся по тяге всего на 2 грамма и что такой винт тянет в 2 раза меньше стандартного трехлопастного винта. По шумности он примерно такой же, только частота шума ниже.

Сейчас хочу отказать от компаса, сразу делать много точек и между ними делать полигоны и сохранять в stl, а так в планах сделать, что питон с нуля строил модель, но идей по этому поводу пока нет.

© Habrahabr.ru