Python-Solvespace 更新

  • 切換群組功能

  • PyDemo.py

Python-Solvespace 更新

最近突然臨時起意,想將 Python-Solvespace 更新後 pull request 給官方,是否有 Python 介面的需求。

有以下特點:

  • 對 Python 友善的介面,且比 C 語言只能使用 slvs.h 方便。

  • 包含最新的 kernel,編譯無須 CMake

不過使用過程中正克服一些障礙,讓功能與 C 語言版本一樣齊全,可以完勝 slvs.h 的 API。

切換群組功能

首先要翻製 CDemo 的範例內容,以讓相關人員可以更快瞭解操作。

不過 CDemo 中分開了群組,因此自由度只會計算該群組的成員

原作者打算讓群組編號 Slvs_hGroup 和 Python int 對應 (mapping),不過這個功能竟然沒做!

直接使用 SWIG 對應可能導致類型混亂,因此我利用一個簡單的轉換式達成:

//"src/slvs_python.hpp"

Slvs_hGroup groupNum(int input) { return (Slvs_hGroup) input; }

SWIG 端口也很簡單,使用 inline 區塊可以不用寫兩行:

//"src/slvs.i"

%inline %{
    extern Slvs_hGroup groupNum(int input);
%}

不過隨後 Python 發出警告:

swig/python detected a memory leak of type 'Slvs_hParam *', no destructor found.

無法找到解構 (destructor) 函式,即 __del__()或 C 語言的 ~some_class() 刪除函式。

之前 Slvs_hParam 等類別是在程式庫中運作,並沒有「顯現」出來讓 Python 操作,首次進入 Python 的記憶體管理範圍中,卻發生沒辦法清除的情況。

這樣會導致程式中止後佔用記憶體,只能靠作業系統清除。

找了一下 Solvespace 的原始碼,這些類別是定義自 uint32_t 整數類別:

//"include/slvs.h"

typedef uint32_t Slvs_hParam;
typedef uint32_t Slvs_hEntity;
typedef uint32_t Slvs_hConstraint;
typedef uint32_t Slvs_hGroup;

而 uint32_t 整數類別是包含自 stdint.h。

SWIG 有針對 C 語言的主要類別做轉換支援,因此在介面檔開頭加上:

//"src/slvs.i"

//Let Python enable to delete Slvs_hParam, Slvs_hEntity, ... types.
%include "stdint.i"

這樣就解決問題了!

函式 groupNum 的使用方法很簡單,即利用一個整數來產生群組,切換群組後即可新增所需項目,最大的特點是可以分開解題(包含自由度)。

sys = System()

#切換至群組 1
g1 = groupNum(1)
sys.default_group = g1

#切換至群組 2
g2 = groupNum(2)
sys.default_group = g2

注意 groupNum 函式與一般 Slvs_hGroup 類別是回傳值,因此相同的輸入值會得到相同的結果,不用像其他實體或約束特意保留指標。

PyDemo.py

完全仿照 CDemo 製成的小腳本,主要展現 Python-Solvespace 應用程式界面精簡的特性。

之前原作者有做 Python 2 版的單元測試腳本,不過還掛載沒用到的套件,因此今天花了一點時間重寫了一下。

原先的註解都搬了過來。

# -*- coding: utf-8 -*-
## Some sample code for slvs.dll. We draw some geometric entities, provide
## initial guesses for their positions, and then constrain them. The solver
## calculates their new positions, in order to satisfy the constraints.
##
## Copyright 2008-2013 Jonathan Westhues.
## Copyright 2016-2017 Yuan Chang [pyslvs@gmail.com] Python-Solvespace bundled.

from slvs import *

sys = System()

'''
An example of a constraint in 3d. We create a single group, with some
entities and constraints.
'''
def Example3d():
    #A point, initially at (x y z) = (10 10 10)
    p0 = sys.add_param(10)
    p1 = sys.add_param(10)
    p2 = sys.add_param(10)
    Point101 = Point3d(p0, p1, p2)

    #and a second point at (20 20 20)
    p3 = sys.add_param(20)
    p4 = sys.add_param(20)
    p5 = sys.add_param(20)
    Point102 = Point3d(p3, p4, p5)

    #and a line segment connecting them.
    LineSegment3d(Point101, Point102)

    #The distance between the points should be 30.0 units.
    Constraint.distance(30., Point101, Point102)

    #Let's tell the solver to keep the second point as close to constant
    #as possible, instead moving the first point.
    Constraint.dragged(Point102)

    #Now that we have written our system, we solve.
    result = sys.solve()
    if result == SLVS_RESULT_OKAY:
        print(
            "okay; now at ({:.3f} {:.3f} {:.3f})\n".format(sys.get_param(0).val, sys.get_param(1).val, sys.get_param(2).val)+
            "             ({:.3f} {:.3f} {:.3f})\n".format(sys.get_param(3).val, sys.get_param(4).val, sys.get_param(5).val)
        )
        print("{} DOF".format(sys.dof))
    else:
        print("solve failed")

'''
An example of a constraint in 2d. In our first group, we create a workplane
along the reference frame's xy plane. In a second group, we create some
entities in that group and dimension them.
'''
def Example2d():
    g1 = groupNum(1)
    sys.default_group = g1

    #First, we create our workplane. Its origin corresponds to the origin
    #of our base frame (x y z) = (0 0 0)
    p0 = sys.add_param(0)
    p1 = sys.add_param(0)
    p2 = sys.add_param(0)
    Point101 = Point3d(p0, p1, p2)

    #and it is parallel to the xy plane, so it has basis vectors (1 0 0)
    #and (0 1 0).
    qw, qx, qy, qz = Slvs_MakeQuaternion(*[1, 0, 0], *[0, 1, 0])
    p3 = sys.add_param(qw)
    p4 = sys.add_param(qx)
    p5 = sys.add_param(qy)
    p6 = sys.add_param(qz)
    Normal102 = Normal3d(p3, p4, p5, p6)

    Workplane200 = Workplane(Point101, Normal102)

    #Now create a second group. We'll solve group 2, while leaving group 1
    #constant; so the workplane that we've created will be locked down,
    #and the solver can't move it.
    g2 = groupNum(2)
    sys.default_group = g2

    #These points are represented by their coordinates (u v) within the
    #workplane, so they need only two parameters each.
    p7 = sys.add_param(10)
    p8 = sys.add_param(20)
    Point301 = Point2d(Workplane200, p7, p8)

    p9 = sys.add_param(20)
    p10 = sys.add_param(10)
    Point302 = Point2d(Workplane200, p9, p10)

    #And we create a line segment with those endpoints.
    Line400 = LineSegment2d(Workplane200, Point301, Point302)

    #Now three more points.
    p11 = sys.add_param(100)
    p12 = sys.add_param(120)
    Point303 = Point2d(Workplane200, p11, p12)

    p13 = sys.add_param(120)
    p14 = sys.add_param(110)
    Point304 = Point2d(Workplane200, p13, p14)

    p15 = sys.add_param(115)
    p16 = sys.add_param(115)
    Point305 = Point2d(Workplane200, p15, p16)

    #And arc, centered at point 303, starting at point 304, ending at
    #point 305.
    Arc401 = ArcOfCircle(Workplane200, Normal102, Point303, Point304, Point305)

    #Now one more point, and a distance
    p17 = sys.add_param(200)
    p18 = sys.add_param(200)
    Point306 = Point2d(Workplane200, p17, p18)

    p19 = sys.add_param(30)
    Distance0 = Distance(Workplane200, p19)

    #And a complete circle, centered at point 306 with radius equal to
    #distance 307. The normal is 102, the same as our workplane.
    Circle402 = Circle(Workplane200, Normal102, Point306, Distance0)

    #The length of our line segment is 30.0 units.
    Constraint.distance(30., Workplane200, Point301, Point302)

    #And the distance from our line segment to the origin is 10.0 units.
    Constraint.distance(10., Workplane200, Point101, Line400)

    #And the line segment is vertical.
    Constraint.vertical(Workplane200, Line400)

    #And the distance from one endpoint to the origin is 15.0 units.
    Constraint.distance(15., Workplane200, Point301, Point101)

    if 0:
        #And same for the other endpoint; so if you add this constraint then
        #the sketch is overconstrained and will signal an error.
        Constraint.distance(18., Workplane200, Point301, Point101)

    #The arc and the circle have equal radius.
    Constraint.equal_radius(Workplane200, Arc401, Circle402)

    #The arc has radius 17.0 units.
    Constraint.diameter(17.*2, Workplane200, Arc401)

    #If the solver fails, then ask it to report which constraints caused
    #the problem.
    sys.calculateFaileds = 1

    #And solve.
    result = sys.solve()
    if result == SLVS_RESULT_OKAY:
        print("solved okay")
        print("line from ({:.3f} {:.3f}) to ({:.3f} {:.3f})".format(
            sys.get_param(7).val, sys.get_param(8).val,
            sys.get_param(9).val, sys.get_param(10).val
        ))
        print("arc center ({:.3f} {:.3f}) start ({:.3f} {:.3f}) finish ({:.3f} {:.3f})".format(
            sys.get_param(11).val, sys.get_param(12).val,
            sys.get_param(13).val, sys.get_param(14).val,
            sys.get_param(15).val, sys.get_param(16).val
        ))
        print("circle center ({:.3f} {:.3f}) radius {:.3f}".format(
            sys.get_param(17).val, sys.get_param(18).val, sys.get_param(19).val
        ))
        print("{} DOF".format(sys.dof))
    else:
        print("solve failed: problematic constraints are:")
        for e in sys.faileds:
            print(e)
        if result == SLVS_RESULT_INCONSISTENT:
            print("system inconsistent")
        else:
            print("system nonconvergent")

if __name__=='__main__':
    #Example3d()
    Example2d()

'''
solved okay
line from (10.000 11.180) to (10.000 -18.820)
arc center (101.114 119.042) start (116.477 111.762) finish (117.409 114.197)
circle center (200.000 200.000) radius 17.000
6 DOF
'''

Comments

comments powered by Disqus