pyembree を windows 上でコンパイルする

# pyembree のコンパイル

embreeは、Intel が開発したレイトレーシングライブラリで、pyembree は、embree のPythonラッパーです。
[pyembree](https://github.com/scopatz/pyembree) を windows 10/11 上で pip install する方法を記録しました。基本的には、
[Install pyembree on Windows without Conda](https://github.com/pyvista/pyvista-support/issues/468) を参照しました。
これについては以下の説明では「参照手順」と表記します。ところどころ差異があったので、全体を記録しておきます。

ちなみに一般的には、`pyembree` は conda-forge でインストールするという説明が多いです。ここでは venv で環境を作り、pip でライブラリーをインストールしたいので、コンパイルした結果を .whl 形式にまとめました。 
 
## pypi に公開されているバージョンについて（追記 2023/3/18）
記事を書いた後で、そもそも `pip install pyembree` で *いちおう* インストールできることが判明しました。
[pyembree のfork が pip install](https://github.com/adam-grant-hendry/pyembree) できるように仕立てられていました。https://github.com/adam-grant-hendry/pyembree  
しかし、この pip install 版では、末尾の pyvista デモの 2 つのうち 1 つしか動きませんでした。
いまのところ、ローカルにコンパイルして .whl からインストールする方が、ベターなようなので、この但し書きをつけて、記事を残しておきます。

## pypi の新しいパッケージ `embreex` (追記 2 2023/6/8)
https://pypi.org/project/embreex/   `pip install embreex` で使えます。
pyvista で使う場合には、trimesh==3.22.0 までアップデートしてインストールし、`pyvista/core/filters
/poly_data.py` の中の `import pyembree` を `import embreex` に書き換える必要があります。(pyvista==0.39.1)
 
## 環境 

windows11 Home 22H2

Python 3.10.9

## build tools 

コンパイラについては、python.jp の[Cコンパイラのインストール](https://www.python.jp/install/windows/install_vstools2017.html)
ページの説明を参照して、ウェブサイトの実情に対応しつつ、導入しました。

```
❯ cd ~
❯ mizut\~ ❯ C:\Users\mizut\AppData\Local\Programs\Python\Python310\python.exe --version
Python 3.10.9
❯ mizut\~ ❯ cl
Microsoft(R) C/C++ Optimizing Compiler Version 19.29.30148 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

使い方: cl [ オプション... ] ファイル名... [ /link リンク オプション... ]
```

注意: x86 の 32 ビットコンパイラーだとリンクエラーします。`cl` の出力を確かめましょう。

```
❯ mizut\~ ❯ cl
Microsoft(R) C/C++ Optimizing Compiler Version 19.29.30148 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

使い方: cl [ オプション... ] ファイル名... [ /link リンク オプション... ]
```

## embree のインストール

`embree-2.17.7.x64.msi`  を Embree の GitHub page からダウンロードしてインストールします。
`pyembree` は `embree2` に対して作られているので、 `embree2` の中から最後のものを選びました。

`C:\Program Files\Intel\Embree v2.17.7 x64` の下に一式がインストールされます。


## Python 仮想環境を作ります。

ここでは、python 3.10.9 で作ります。
あとでコンパイラを使うので、Developer Powershell for VS 2019 のターミナルで作業します。
（これが、手元でうまく動いたのは、Windows Terminal から x64 の設定を呼んでいたからのようです。
build tools をインストールしただけであれば、`x64 Native Tools Command Prompt for VS 2019` というコマンドプロンプトを使うのが良いようです）

参照手順の 4. と 5. です。ターミナルはこの後も継続的に使っていきます。

```pwsh
❯ ~ ❯ .\AppData\Local\Programs\Python\Python310\python.exe -m venv --upgrade-dep py310/pyembree
❯ ~ ❯ .\py310\pyembree\Scripts\Activate.ps1
(pyembree) ❯ mizut\~ ❯ pip install numpy cython wheel setuptools pyvista pykdtree rtree trimesh
```


## pyembree を自分のリポジトリに fork してから、これを手元に clone

参照手順の 6. に相当します。参照手順では、site-packages の下で作業をすると書いてありますが、別の場所で作業することにします。
pyembree を自分のリポジトリに fork してから、これを手元に clone しました。
適当な名前でブランチを切って作業することにします。

```pwsh
(pyembree) ❯ ~ ❯ cd C:\Users\mizut\OneDrive\Projects\pyembree\

(pyembree) ❯ ~\OneDrive\Projects\pyembree ❯ git clone git@github.com:(my_repositly).git
(pyembree) ❯ ~\OneDrive\Projects\pyembree ❯ cd pyembree
(pyembree) ❯ ~\OneDrive\Projects\pyembree\pyembree ❯ git branch winbuild
(pyembree) ❯ ~\OneDrive\Projects\pyembree\pyembree ❯ git checkout winbuild
```

## ビルドの準備作業

`setup.py` を書き換えます。参照手順通りです。

```python
import os
from setuptools import find_packages, setup

import numpy as np
from Cython.Build import cythonize
from Cython.Distutils import build_ext


include_path = [
    np.get_include(),
]

ext_modules = cythonize("pyembree/*.pyx", language_level=3, include_path=include_path)

for ext in ext_modules:
    ext.include_dirs = include_path
    ext.libraries = [
        "pyembree/embree2/lib/embree",
        "pyembree/embree2/lib/tbb",
        "pyembree/embree2/lib/tbbmalloc",
    ]

setup(
    name="pyembree",
    version="0.1.6",
    cmdclass={"build_ext": build_ext},
    ext_modules=ext_modules,
    zip_safe=False,
    packages=find_packages(),
    include_package_data=True,
    package_data={"pyembree": ["*.cpp", "*.dll"]},
)
```

インストール済みの embree フォルダーから、`.lib` と `.dll` をコピーします。

| ファイル・フォルダ                                | コピー元                                                      | コピー先                            | 参考                                     |
|------------------------------------------|-----------------------------------------------------------|---------------------------------|----------------------------------------|
| embree2 フォルダ                             | C:\Program Files\Intel\Embree v2.17.7 x64\include\embree2 | (projectroot)\pyembree\pyembree |                                        |
| lib フォルダ                                 | C:\Program Files\Intel\Embree v2.17.7 x64\lib             | (projectroot)\pyembree\pyembree\embree2 | 中身は embree.lib, tbb.lib, tbbmalloc.lib |
| embree.dll, tbb.dll, tbbmalloc.dll | C:\Program Files\Intel\Embree v2.17.7 x64\bin             | (projectroot)\pyembree\pyembree |

## ファイル配置の確認

参照手順の 11. に記載されてるの同じファイル配置になっているでしょうか？ git clone で作業していると、もう少し余分なフォルダもありますが、必要な部分は同じになっています。

```txt
pyembree
│   .authors.yml
│   .gitignore
│   .mailmap
│   .travis-install.sh
│   .travis.yml
│   AUTHORS
│   CHANGELOG.rst
│   LICENSE
│   MANIFEST.in
│   README.rst
│   rever.xsh
│   setup.py
│   
├───examples
│       attenuate.py
│       intersection.py
│       
├───news
│       TEMPLATE.rst
│       
├───pyembree
│   │   embree.dll
│   │   mesh_construction.h
│   │   mesh_construction.pyx
│   │   rtcore.pxd
│   │   rtcore.pyx
│   │   rtcore_geometry.pxd
│   │   rtcore_geometry_user.pxd
│   │   rtcore_ray.pxd
│   │   rtcore_scene.pxd
│   │   rtcore_scene.pyx
│   │   tbb.dll
│   │   tbbmalloc.dll
│   │   triangles.pyx
│   │   __init__.pxd
│   │   __init__.py
│   │
│   └───embree2
│       │   rtcore.h
│       │   rtcore.isph
│       │   rtcore_builder.h
│       │   rtcore_geometry.h
│       │   rtcore_geometry.isph
│       │   rtcore_geometry_user.h
│       │   rtcore_geometry_user.isph
│       │   rtcore_ray.h
│       │   rtcore_ray.isph
│       │   rtcore_scene.h
│       │   rtcore_scene.isph
│       │   rtcore_version.h
│       │
│       └───lib
│               embree.lib
│               tbb.lib
│               tbbmalloc.lib
│
├───recipes
│   └───pyembree
│           build.sh
│           meta.yaml
│
└───tests
        test_intersection.py
```

## ファイル編集 

(projectroot)\pyembree\pyembree にあるすべての `*.pyx` と `*.pxd` ファイルに、以下の編集を行います。

先頭に、以下の行を加えます。

```python
# distutils: language=c++
```

rtcore module の読み込み部をすべて書き換えます。
例えば、

```python
cimport rtcore as rtc
```

を

```python
cimport pyembree.rtcore as rtc
```

にします。

`rtcore_ray` とか `rtcore_geometry` とか、`from rtcore cimport ....`  などがあります。

## ファイル追加

ファイル `MANIFEST.in` を トップレベルの pyembree フォルダに作ります。

```txt
include *.h *.pxd *.dll
recursive-include ./embree2 *.h *.isph
recursive-include ./embree2/lib *.lib
```

## ビルド

```pwsh
(pyembree) ❯ ~\OneDrive\Projects\pyembree\pyembree ❯ python setup.py build_ext -i
```

Warning は出ますが、ビルドできます。

* Developer powershell のコンパイラが 64 ビットにならない場合、StartMenu の VisualStudio2019 の中で `x64 Native Tools Command Prompt for VS 2019` というコマンドプロンプトを使うのが良いようです。
* Windows Terminal で 64 ビットコンパイラの Powershell を開く設定についての、記事もあります。[Developer Powershell for VS 2019をWindows Terminalから開く](https://zenn.dev/shiena/articles/developer-pwsh-terminal)
  

## ビルドの確認

以下のようにして、その場で import を試みます。

```pssh
(pyembree) ❯ ~\OneDrive\Projects\pyembree\pyembree ❯ python
Python 3.10.9 (tags/v3.10.9:1dd9be6, Dec  6 2022, 20:01:21) [MSC v.1934 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyembree
>>> from pyembree import rtcore_scene
>>> exit()
```

## wheel package を作る

参照手順には書いてありませんが、インストール用の `.whl` を作ります。

```pwsh
(pyembree) ❯ ~\OneDrive\Projects\pyembree\pyembree ❯ python setup.py bdist_wheel
```
`dist\pyembree-0.1.6-cp310-cp310-win_amd64.whl` が生成されるので、このファイルを任意の場所に置いて `pip install pyembree-0.1.6-cp310-cp310-win_amd64.whl` でインストールが可能です。

```
pip install pyembree-0.1.6-cp310-cp310-win_amd64.whl
```

## demo

動作確認の例です。

Pyvista の Optional Features のページのデモが動きます。
[Optional Features](https://docs.pyvista.org/user-guide/optional_features.html)

1 つ目の例:
```python
from math import sin, cos, radians
import pyvista as pv

# Create source to ray trace
sphere = pv.Sphere(radius=0.85)

# Define a list of origin points and a list of direction vectors for each ray
vectors = [ [cos(radians(x)), sin(radians(x)), 0] for x in range(0, 360, 5)]
origins = [[0, 0, 0]] * len(vectors)

# Perform ray trace
points, ind_ray, ind_tri = sphere.multi_ray_trace(origins, vectors)

# Create geometry to represent ray trace
rays = [pv.Line(o, v) for o, v in zip(origins, vectors)]
intersections = pv.PolyData(points)

# Render the result
p = pv.Plotter()
p.add_mesh(sphere,
           show_edges=True, opacity=0.5, color="w",
           lighting=False, label="Test Mesh")
p.add_mesh(rays[0], color="blue", line_width=5, label="Ray Segments")
for ray in rays[1:]:
    p.add_mesh(ray, color="blue", line_width=5)
p.add_mesh(intersections, color="maroon",
           point_size=25, label="Intersection Points")
p.add_legend()
p.show()
```

2 つ目の例: (3 行目の `from tqdm import tqdm` はコメントアウトしました)

```python
import numpy as np
from pykdtree.kdtree import KDTree
# from tqdm import tqdm

import pyvista as pv
from pyvista import examples

# Load data
data = examples.load_random_hills()
data.translate((10, 10, 10), inplace=True)

# Create triangular plane (vertices [10, 0, 0], [0, 10, 0], [0, 0, 10])
size = 10
vertices = np.array([[size, 0, 0], [0, size, 0], [0, 0, size]])
face = np.array([3, 0, 1, 2])

planes = pv.PolyData(vertices, face)

# Subdivide plane so we have multiple points to project to
planes = planes.subdivide(8)

# Get origins and normals
origins = planes.cell_centers().points
normals = planes.compute_normals(cell_normals=True, point_normals=False)["Normals"]

# Vectorized Ray trace
points, pt_inds, cell_inds = data.multi_ray_trace(
    origins, normals
)  # Must have rtree, trimesh, and pyembree installed

# Filter based on distance threshold, if desired (mimics VTK ray_trace behavior)
# threshold = 10  # Some threshold distance
# distances = np.linalg.norm(origins[inds] - points, ord=2, axis=1)
# inds = inds[distances <= threshold]

tree = KDTree(data.points.astype(np.double))
_, data_inds = tree.query(points)

elevations = data.point_data["Elevation"][data_inds]

# Mask points on planes
planes.cell_data["Elevation"] = np.zeros(planes.n_cells)
planes.cell_data["Elevation"][pt_inds] = elevations

# Create axes
axis_length = 20
tip_length = 0.25 / axis_length * 3
tip_radius = 0.1 / axis_length * 3
shaft_radius = 0.05 / axis_length * 3
x_axis = pv.Arrow(
    direction=(axis_length, 0, 0),
    tip_length=tip_length,
    tip_radius=tip_radius,
    shaft_radius=shaft_radius,
    scale="auto",
)
y_axis = pv.Arrow(
    direction=(0, axis_length, 0),
    tip_length=tip_length,
    tip_radius=tip_radius,
    shaft_radius=shaft_radius,
    scale="auto",
)
z_axis = pv.Arrow(
    direction=(0, 0, axis_length),
    tip_length=tip_length,
    tip_radius=tip_radius,
    shaft_radius=shaft_radius,
    scale="auto",
)
x_label = pv.PolyData([axis_length, 0, 0])
y_label = pv.PolyData([0, axis_length, 0])
z_label = pv.PolyData([0, 0, axis_length])
x_label.point_data["label"] = [
    "x",
]
y_label.point_data["label"] = [
    "y",
]
z_label.point_data["label"] = [
    "z",
]

# Plot results
p = pv.Plotter()
p.add_mesh(x_axis, color="r")
p.add_point_labels(x_label, "label", show_points=False, font_size=24)
p.add_mesh(y_axis, color="r")
p.add_point_labels(y_label, "label", show_points=False, font_size=24)
p.add_mesh(z_axis, color="r")
p.add_point_labels(z_label, "label", show_points=False, font_size=24)
p.add_mesh(data)
p.add_mesh(planes)
p.show()
```