【CAD算法】【遺傳算法】採用遺傳算法優化Coons Patch曲面程序(python實現)[4]

1.內容回顧

關於Coons Patch的定義和生成程序,請參考https://blog.csdn.net/iwanderu/article/details/103624398, 本節內容會部分沿用上一節代碼。

關於遺傳算法,看到很多高讚的文章,都很清晰的講解了遺傳算法的邏輯和思路,但是很少看到可以直接參考的代碼。實際上,遺傳算法它的可移植性很高,它的雜交部分,基因突變部分還有循環部分基本上所有的遺傳算法都是類似的,解決不同問題的代碼部分的區別主要在fitness函數的定義上還有染色體(字符串)的生成邏輯上。

遺傳算法可以用以下的僞代碼進行抽象:

在這裏插入圖片描述
在第4部分,遺傳算法實現的具體代碼將會依次按步驟講解。由於這是解決Coons Patch表面優化,所以在第3部分會講解相關的背景代碼,相關內容請參考https://blog.csdn.net/iwanderu/article/details/103624398。

2.題目描述

In this project you will use Genetic Algorithm to find the blending functions alpha(u) and beta(v) of Coons patch to construct a minimal-area Coons patch of four curves. The specifics are as follows.
(1) The four input curves are Bezier curves of degree 5 (6 control points).
(2) The alpha(u) (also beta(v)) can be modeled as a Bezier curve of degree 3 with its first and fourth control point on (0, 1) and (1, 0) respectively. Also, let the u of the 2nd and 3rd control point be 0.333333 and 0.666667 respectively. Then, the four control points of alpha(u) are (0, 1), (0.333333, alpha1), (0.666667, alpha2), and (1, 0); alpha1 and alpha2 are therefore the optimization variables (so are beta1 and beta2 for beta(v) ).
(3) You need to numerically calculate the surface area of any Coons patch. Refer to Lab 3.(3).
(4) You now have four variables alpha1, alpha2, beta1, beta2} to search and the optimization objective is the surface area of the corresponding Coons patch. For simplification, you can assume that the range for alpha1, alpha2, beta1, beta2} is [0, 2].
(5) In addition to the surface area, you can also try other types of optimization objectives, e.g., to minimize the maximum magnitude of the principle curvatures |κ1| and |κ2| of the surface (you need to numerically calculate κ1 and κ2).
要求如下,給定一個Coons Patch曲面,其中它的四個邊界曲線是已知的(用戶輸入),在確定的邊界曲線下,Coons曲面仍然有多種可能的形狀,這個形狀是通過Blending function alpha(u)和beta(v)來控制的。這兩個Blending function都受4個控制點約束,其中有兩個控制點的x座標是未知的,分別是alpha1和alpha2。因此,對於這兩個函數而言,一共是有4個未知量。那麼需要通過遺傳算法來確定一個最佳的alpha1/2和beta1/2使得這個Coons曲面的面積最小。很明顯,優化方向給定了,優化變量給定了,那麼這是一個簡單的少變量遺傳算法問題。根據定義,這四個待優化的數取值範圍都是在0-1之間的。

3.Coons曲面的獲取

3.1頭文件的引用和變量定義

頭文件如下

import numpy as np
import matplotlib.pyplot as plt
import math
from mpl_toolkits.mplot3d import Axes3D
import random

所需要的變量如下

#parameters
N = 10 #mesh size of coons patch
step = 0.1#distance between neighbor mesh
value_type = np.float64
f = np.array([],dtype=value_type) #temp: shape (-1,3) all the boundary points
alpha_u = np.array([],dtype=value_type) #blending function: shape (N,1)
beta_v = np.array([],dtype=value_type)#blending function: shape (N,1)
mash_grid = np.array([],dtype=value_type) #mesh of coons patch
surface = np.array([],dtype=value_type) #coons patch

注意這個N,我們希望用100個三角形的mesh來描述這個曲面,所以在一個方向上的採樣是10.

3.2Blending Function的定義

# the following function is to determine alpha(u) and beta(v)
def buildBlendingFunction(control_point,u):
    #u is current step
    #control_point is a np.array,the shape should be (2,2)=>2 points, x-y(or called u-alpha) coordinates
    #return value is a scaler => alpha(u)
    P = np.array([0,1],dtype=value_type)
    P = np.append(P,control_point)
    P = np.append(P,[1,0])
    P = P.reshape((-1,2))
    alpha = np.array([],dtype=value_type) #shape should be (1,2)
    alpha = (1-u)**3 * P[0] + 3 * (1-u)**2 * u * P[1] + 3 * (1-u) * u**2 * P[2] + u**3 * P[3]
    return alpha[0]

3.3 Coons曲面的獲取

嚴格根據定義來確定,注意不要弄混Q1Q2P1P2.在這裏,我是按照下圖的順序定義各個邊界曲線的。
在這裏插入圖片描述

#define coons patch
def coonsPatch(Q1,Q2,P1,P2,alpha_u,beta_v,N):
    surface_item = np.array([],dtype=value_type)
    Q00 = P1[0]
    Q01 = Q2[0]
    Q10 = P2[0]
    Q11 = P2[N]
    surface = np.array([],dtype=value_type)
    for u in range(N+1):
        alpha  = alpha_u[u]
        for v in range(N+1):
            beta = beta_v[v]
            surface_item = alpha*Q1[v]+(1-alpha)*Q2[v] + beta*P1[u]+(1-beta)*P2[u] - (beta*(alpha*Q00+(1-alpha)*Q01)+(1-beta)*(alpha*Q10+(1-alpha)*Q11))
            surface = np.append(surface,surface_item)
    surface = surface.reshape((-1,3))
    return surface

3.4獲取Bezier曲線

四個邊界曲線是按照Bezier曲線來定義的,因此它的代碼如下:

# get 4 boundary bazier curves 
def getCurve(closedOrOpen,pointsNumber,point,point_index,u):
    C = []
    n = pointsNumber - 1 # n is fewer in numbers than the total control points number. According to definition.
    point_show = np.array([],dtype=np.float64)
    for i in range(n+1):
        point_show = np.append(point_show,point[point_index + i])  
    if (closedOrOpen == 0): # if it is closed, means the oth and nth control points are the same.
        n += 1
        point_show = np.append(point_show,point[point_index])
    elif (closedOrOpen == 1):
        pass
    point_show = point_show.reshape((-1,3))
    if ((n+1) % 2 == 0):
        for i in range((n+1) / 2):
            up = 1
            down = 1
            j = n
            while (j > i):
                up *= j
                j = j - 1
            j = n - i
            while (j > 0):
                down *= j
                j = j - 1
            C.append(up / down)
    elif ((n+1) % 2 == 1):
        for i in range(n / 2):
            up = 1
            down = 1
            j = n
            while (j > i):
                up *= j
                j = j - 1
            j = n - i
            while (j > 0):
                down *= j
                j = j - 1
            C.append(up / down)
        up = 1
        down = 1
        j = n
        while (j > n/2):
            up *= j
            j = j - 1
        j = n/2
        while (j > 0):
            down *= j
            j = j - 1
        C.append(up / down)
    if (n%2 == 1):
        for i in range(int((n+1)/2)):
            C.append(C[int(n/2-i)])
    if (n%2 == 0):
        for i in range(int((n+1)/2)):
            C.append(C[int(n/2-i-1)])
    global f
    fx = 0
    fy = 0 #not this place!!
    fz = 0
    for i in range(n+1):
        fx += C[i] * u**i * (1-u)**(n-i) * point_show[i][0]
        fy += C[i] * u**i * (1-u)**(n-i) * point_show[i][1]
        fz += C[i] * u**i * (1-u)**(n-i) * point_show[i][2]
    list = []
    list.append(fx)
    list.append(fy) 
    list.append(fz)
    array_list = np.array([list],dtype=np.float64) 
    f = np.append(f,array_list)

3.5 可視化

#visualize the coons patch
def visualize(alpha1,alpha2,beta1,beta2,N,show_flag):
    # if show_flag == 1, show the coons patch
    fig = plt.figure()
    ax = fig.gca(projection='3d')

第一步,先傳入各個參數

    #1.control point for blending function alpha(u) and beta(v)
    control_point1 = np.array([0.333333,alpha1,0.666667,alpha2],dtype=value_type)
    control_point1 = control_point1.reshape((2,2))
    control_point2 = np.array([0.333333,beta1,0.666667,beta2],dtype=value_type)
    control_point2 = control_point1.reshape((2,2))
    #2.control point for boundary curves(they should share 4 edge points!)
    point_curve1 = np.array([-10,10,10,-6,7,9,-2,7,5,2,8,9,6,11,11,10,10,10],dtype=value_type)
    point_curve1 = point_curve1.reshape((-1,3)) #P2
    point_curve2 = np.array([10,-10,10,6,6,13,9,-2,3,7,2,5,13,6,0,10,10,10],dtype=value_type)
    point_curve2 = point_curve2.reshape((-1,3)) #Q2
    point_curve3 = np.array([-10,-10,10,-6,-11,11,-2,-8,9,2,-7,5,6,-7,9,10,-10,10],dtype=value_type)
    point_curve3 = point_curve3.reshape((-1,3)) #P1
    point_curve4 = np.array([-10,-10,10,-13,3,0,-7,-2,-5,-9,2,3,-6,6,9,-10,10,10],dtype=value_type)
    point_curve4 = point_curve4.reshape((-1,3)) #Q1

傳入參數後,就可以調用之前定義好的程序,來生成邊界曲線,注意,邊界曲線是通過N+1個在曲線上的點來描述的,他們都放在array裏保存。

    #4.get boundary curves, all the boundary points will array in f repeatedly.  
    global Q1,Q2,P1,P2,f
    Q1 = np.array([],dtype=value_type)
    Q2 = np.array([],dtype=value_type)
    P1 = np.array([],dtype=value_type)
    P2 = np.array([],dtype=value_type)
    f = np.array([],dtype=value_type)
    u = 0
    for i in range(N+1):
        getCurve(1,6,point_curve1,0,u)
        u += step
    P2 = np.append(P2,f)
    f = np.array([],dtype=value_type)
    u = 0
    for i in range(N+1):
        getCurve(1,6,point_curve2,0,u)
        u += step
    Q2 = np.append(Q2,f)
    f = np.array([],dtype=value_type)
    u = 0
    for i in range(N+1):
        getCurve(1,6,point_curve3,0,u)
        u += step
    P1 = np.append(P1,f)
    f = np.array([],dtype=value_type)
    u = 0
    for i in range(N+1):
        getCurve(1,6,point_curve4,0,u)
        u += step
    Q1 = np.append(Q1,f)
    f = np.array([],dtype=value_type)
    Q1 = Q1.reshape((-1,3))
    Q2 = Q2.reshape((-1,3))
    P1 = P1.reshape((-1,3))
    P2 = P2.reshape((-1,3))

可以把這四條邊界曲線顯示出來,代碼如下

    plt.plot(Q1[:,0],Q1[:,1],Q1[:,2],'r.')
    plt.plot(Q2[:,0],Q2[:,1],Q2[:,2],'r.')
    plt.plot(P1[:,0],P1[:,1],P1[:,2],'r.')
    plt.plot(P2[:,0],P2[:,1],P2[:,2],'r.')
    plt.plot(Q1[:,0],Q1[:,1],Q1[:,2],'-')
    plt.plot(Q2[:,0],Q2[:,1],Q2[:,2],'-')
    plt.plot(P1[:,0],P1[:,1],P1[:,2],'-')
    plt.plot(P2[:,0],P2[:,1],P2[:,2],'-')

之後,把coonspatch曲面顯示出來,

    #6.show the surface
    global surface,mash_grid 
    surface = np.array([],dtype=value_type)
    mash_grid = np.array([],dtype=value_type)
    surface = coonsPatch(Q1,Q2,P1,P2,alpha_u,beta_v,N)
    surface = np.reshape(surface,(-1,3))
    count = N
    for i in range(count):
        for j in range(count+1):
        	mash_grid = np.append(mash_grid,surface[(count+1)*i+j])
        	mash_grid = np.append(mash_grid,surface[(count+1)*(i+1)+j])
    mash_grid = np.reshape(mash_grid,(-1,3))
    for i in range(count):
        plt.plot(surface[(count+1)*i:(count+1)*(i+1),0],surface[(count+1)*i:(count+1)*(i+1),1],surface[(count+1)*i:(count+1)*(i+1),2],'-')
        plt.plot(mash_grid[2*(count+1)*i:2*(count+1)*(i+1),0],mash_grid[2*(count+1)*i:2*(count+1)*(i+1),1],mash_grid[2*(count+1)*i:2*(count+1)*(i+1),2],'-') 
	#pass

效果如下,用mesh的方法來描述曲面。注意,mesh也是CAD的一個重要算法,對物體合理的劃分,是進行有限元分析的前提條件。
在這裏插入圖片描述
在這裏插入圖片描述
最後,還有一個是否顯示曲線的判斷,因爲遺傳算法它是有很多循環的,沒有必要每次都顯示出曲線,因此需要一個判斷,這些代碼組成了getCurve()函數。

    if show_flag == 1:
        ax.legend()
        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.set_zlabel('Z')
        plt.show()

最後一個是Coons曲面表面積計算方法了。這裏,體現了mesh網格的作用了。根據有限元的思想,一個曲面可以分割爲若干個三角形來近似表示,那這些三角形面積的和就是Coons曲面的面積了。所以這個算法的核心就是確定三角形的頂點座標和三角形面積的計算公式。

#calculate the area of coons patch
def countTriangleArea(surface,N):
    area = 0
    for i in range(N):
        for j in range(N):
            x1,y1,z1 = surface[(N+1)*i+j][0],surface[(N+1)*i+j][1],surface[(N+1)*i+j][2]
            x2,y2,z2 = surface[(N+1)*(i+1)+j][0],surface[(N+1)*(i+1)+j][1],surface[(N+1)*(i+1)+j][2]
            x3,y3,z3 = surface[(N+1)*i+j+1][0],surface[(N+1)*i+j+1][1],surface[(N+1)*i+j+1][2]
            sides0 = ((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)**0.5
            sides1 = ((x1 - x3)**2 + (y1 - y3)**2 + (z1 - z3)**2)**0.5
            sides2 = ((x3 - x2)**2 + (y3 - y2)**2 + (z3 - z2)**2)**0.5 	        
            p = (sides0 + sides1 + sides2) / 2
            area += (p * (p - sides0) * (p - sides1) * (p - sides2))**0.5
            
            x1,y1,z1 = surface[(N+1)*(i+1)+1+j][0],surface[(N+1)*(i+1)+1+j][1],surface[(N+1)*(i+1)+1+j][2] 
            sides0 = ((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)**0.5
            sides1 = ((x1 - x3)**2 + (y1 - y3)**2 + (z1 - z3)**2)**0.5
            sides2 = ((x3 - x2)**2 + (y3 - y2)**2 + (z3 - z2)**2)**0.5 	        
            p = (sides0 + sides1 + sides2) / 2
            area += (p * (p - sides0) * (p - sides1) * (p - sides2))**0.5
    return area

繪圖的時候,每個曲線用N+1個點來描述,那麼就在這個方向上把曲面分成了N份,兩個方向上就是N^2份,每一份有2個mesh三角形,所以Coons曲面一共有2N**2個三角形。你可以數一下,這裏N取10.
在這裏插入圖片描述

4.遺傳算法

接下來,就是激動人心的遺傳算法部分了,不着急,一步步來。

4.1變量

##########PART II: GENETIC ALGORITHM##########
#parameters
S = [] #initial population S
S_next = [] #next population S
fitness = [] #the fitness of each individual in S
fitness_percentage = [] #the percentage of the total fitness for each s
POPULATION = 10 #the size n of the population, and POPULATION % 2 == 0
ITERATION  = 10 #the maximum running time not exceeded
S_LENGTH = 32 #length of the solution X by a binary string s, and S_LENGTH % 4 == 0

遺傳算法需要一組染色體,每一個染色體是用二進制01組成的string來描述的。大S是一個列表,存放着隨機生成的初始化的所有染色體的集合,染色體數量定義爲POPULATION。經過一次迭代後,下一代染色體存放在S_next中。fitness_percentage是每次篩選的重要原則,它依次存放着每個染色體被選中的概率。在本題中,一共有四個變量,而且這四個變量都是在0-1之間的,這裏用8個二進制來表示其中一個變量,那麼四個就需要32位二進制數,其中1-8位表示alpha1,9-16位表示alpha2,以此類推。據一個例子,如果此時alpha的字符串(101101101)2進制,那麼它表示的alpha的值就是101101101除以11111111對應的值,通過這種方法,可以把染色體字符串解碼爲十進制的數值,也可以把十進制的結果編碼爲對應的字符串。類似於下面PPT的方法。
在這裏插入圖片描述

4.2染色體的解碼和編碼

首先涉及到二進制和十進制之間的轉換,代碼如下,參考了https://www.cnblogs.com/ddpeng/p/11302368.html ,表示感謝。

#Decimal system->Binary system
def decToBin(num):
    arry = []   #
    while True:
        arry.append(str(num % 2))  #
        num = num // 2   
        if num == 0:    
            break
    return "".join(arry[::-1]) 

#Binary system->Decimal system
def binToDec(binary):
    result = 0   #
    for i in range(len(binary)):   
        result += int(binary[-(i + 1)]) * pow(2, i)
    return result

因爲也涉及到alpha的值就是101101101除以11111111的計算,所以需要定義一個函數,輸入二進制的位數,能夠輸出對應二進制數最大值的十進制數。具一個例子,輸入3,那麼最大的二進制數就是111,返回它的十進制就是7。

#find the decimal number for full binary number(e.g. input 3->111->7)
def fullBinToDec(length):
    sum = 0
    for i in range(length):
        sum = sum * 2 + 1
    return sum

下一步,根據之前介紹的那樣,進行編碼和解碼。

 #recover from s to alpha1,alpha2,beta1,beta2
def decoding(s,length):
     #s->each individual in S
     #length-> length of each s
     #alpha1,alpha2,beta1,beta2 belongs to [0,1]
     alpha1 =  float(binToDec(s[0:length/4])) / float(fullBinToDec(length/4))
     alpha2 =  float(binToDec(s[length/4:length/2])) / float(fullBinToDec(length/4))
     beta1 =  float(binToDec(s[length/2:length*3/4])) / float(fullBinToDec(length/4))
     beta2 =  float(binToDec(s[length*3/4:length])) / float(fullBinToDec(length/4))
     return alpha1,alpha2,beta1,beta2

#code alpha1,alpha2,beta1,beta2 to s
def coding(alpha1,alpha2,beta1,beta2,length):
     alpha1 = int(alpha1*fullBinToDec(length/4))
     alpha2 = int(alpha2*fullBinToDec(length/4)) 
     beta1 = int(beta1*fullBinToDec(length/4))
     beta2 = int(beta2*fullBinToDec(length/4))    
     return decToBin(alpha1)+decToBin(alpha2)+decToBin(beta1)+decToBin(beta2)

4.3第一代染色體的生成

到這裏開始,就是遺傳算法的核心部分了。第一代染色體首先一定要是隨機生成的,因爲知道它染色體的位數是32位,那麼32個1對應的二進制對應的十進制數,設它爲M,那麼取0-M的隨機數,然後轉爲二進制,就隨機初始化了第一代染色體。

 #generate S
def generateInitialS(length,size):
    #length-> length of each s
    #size->size of the population
    global S
    for i in range(size):
        rand = random.randint(0,fullBinToDec(length))
        rand_bi = decToBin(rand)
        temp = ""
        for j in range(length-len(rand_bi)):
            temp += "0"
        rand_bi = temp + rand_bi
        S.append(rand_bi)

4.4 fitness函數的構建

當然了,進化的方向就是自然選擇的方向。在這裏,我們希望表面積最小,那麼可以構造一個fitness = 1 / Area作爲fitness函數,通過優化這個函數來達到自然選擇,也就是優化的目的。

#Calculate the fitness of each individual in S
def  fitnessCalculator(S,length,N,show_flag):
    #population S: list
    #length-> length of each s
    #N->mesh size of coons patch
    # if show_flag == 1, show the coons patch
    fitness = []
    fitness_percentage = []
    fitness_sum = 0
    for i in range(len(S)):
        #recover from s to alpha1,alpha2,beta1,beta2
        alpha1,alpha2,beta1,beta2 = decoding(S[i],length)
        #print(S[i])
        print("alpha1:",alpha1)
        print("alpha2:",alpha2)
        print("beta1:",beta1)
        print("beta2:",beta2)
        #get area of coons patch
        visualize(alpha1,alpha2,beta1,beta2,N,show_flag)
        show_flag = 0 #only once
        #calculate the area of coons patch
        area = countTriangleArea(surface,N)
        print("area is ",area) 
        #calculate fitness
        fitness.append(1/area)
        fitness_sum += 1/area   
    for i in range(len(S)):
        fitness_percentage.append(fitness[i] / fitness_sum)
    return fitness,fitness_percentage #list

4.5自然選擇

自然選擇很自然就是按照輪盤賭的方式進行。根據fitness的計算,我們可以知道不同染色體它適應環境的概率是不一樣的,概率大的會更容易被保留下來。這裏思路如下,假如說有10個基因,他們的概率分別是0.1,0.1,0.1,…,fitness_percentage_add存放着依次的概率和,也就是0.1,0.2,0.3,…,此時在0-1隨機生成一個數,比如說0.11,比0.1大比0.2小,那麼此時自然選擇了第二個染色體到下一代,以此類推。因此,在fitness中概率大的染色體更容易被保留。

#natural selection 
def naturalSelection(S,fitness,fitness_percentage):
    #Weighted Roulette Wheel
    fitness_percentage_add = []
    S_next = []
    fitness_percentage_add.append(fitness_percentage[0])
    for i in range(len(S)-1):
        fitness_percentage_add.append(fitness_percentage[i+1]+fitness_percentage_add[i])
    for i in range(len(S)):
        rand = random.random() #0-1
        for j in range(len(S)):
            if(rand < fitness_percentage_add[j]):
                S_next.append(S[j])
                break
    return S_next

4.6 交配

在自然中,動物繁殖涉及到父母染色體的隨機組合,那麼就需要交換染色體。在這裏,策略是在染色體的隨機一個位置後的染色體全部交換。這個函數封裝了一次交換。對於N個羣體而言,一共要交換N/2次。

#reproduction
def reproduction(S):
    global S_next
    rand1 = random.randint(0,len(S)-1)
    rand2 = random.randint(0,len(S)-1)
    rand3 = random.randint(0,len(S[0])-1)
    s1_list = list(S[rand1])
    s2_list = list(S[rand2])
    #crossover
    for i in range(len(S[0])-rand3):
        temp = s1_list[rand3+i]
        s1_list[rand3+i] = s2_list[rand3+i]
        s2_list[rand3+i] = temp
    s1 = ""
    s2 = ""
    for i in range(len(s1_list)):
        s1 += s1_list[i]
        s2 += s2_list[i]
    S_next.append(s1)
    S_next.append(s2)

在這裏插入圖片描述

注意,開始交換的位置是隨機的。

4.7基因突變

在自然界中,基因突變廣泛存在引入新的性狀。在這裏,存在0.1%的概率在染色體的隨機位置發生基因突變。

#mutation
def mutation():
    global S
    for i in range(len(S)):
        rand = random.random() #0-1
        if rand < 0.001:
            s = list(S[i])
            rand = random.randint(0,len(s)-1)
            if(s[rand] == 0):
                s[rand] = 1
            else:
                s[rand] = 0
            s1 = ""
            for i in range(len(s)):
                s1 += s[i]
            S[i] = s1

4.8遺傳算法的主體部分

完成所有功能的函數封裝,在這裏可以直接調用循環優化了。

def geneticAlgorithm(N,iteration):
    global S,S_next
    show_flag = 0 # not show the figure
    for i in range(iteration):
        print("############The round of iteration is,",i,"###########")
        #calculate fitness
        fitness,fitness_percentage = fitnessCalculator(S,len(S[0]),N,show_flag)
        show_flag = 0 # not show the figure
        #natural selection 
        S = naturalSelection(S,fitness,fitness_percentage)
        #reproduction
        S_next = []
        for j in range(len(S)/2):            
            reproduction(S) 
        S = S_next
        #mutation
        mutation()
        #visualize
        if(i % 5 == 0):
            show_flag = 1 #show the figure
        print("#############################################")

4.9main()函數

generateInitialS(S_LENGTH,POPULATION)
geneticAlgorithm(N,ITERATION)

分別是初始化染色體population,然後執行遺傳算法,至此,全部完成。很簡單吧。參考文獻是香港科技大學機械工程專業CAD/CAE/CAM課程課件。
所有代碼的鏈接如下:https://github.com/iwander-all/CAD.git

參考文獻
K. TANG, Fundamental Theories and Algorithms of CAD/CAE/CAM

發佈了32 篇原創文章 · 獲贊 55 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章