Jack Huang's Blog


  • 首页

  • 标签

  • 归档

  • 搜索

CPlusPlus单元测试框架Catch入门

发表于 2020-01-15 | 更新于 2022-07-11

最近在编写一个飞行力学的类库,随着类数量的增加,代码越来越复杂,质量越来越难以控制,因此引入单元测试,通过自动化测试以保障代码质量,防止因代码修改引入新的Bug。C++已经有一些成熟的代码测试框架,例如:Google Test, Boost.Test, CppUnit, Cute,等等。通过综合分析和比较,最终选择Catch2测试框架。选择该测试框架的原因是其够轻量级,够简单。

Catch2简介

Catch2是轻量级的C++的多范式测试框架。 它也支持Objective-C(也许还有C)。 它主要作为单个头文件分发,尽管某些扩展可能需要其他头文件。

关键特征

  • 快速且非常容易上手。 只需下载catch.hpp,#include它就可以了。
  • 没有外部依赖性。 只要您可以编译C ++ 11并拥有C ++标准库即可。
  • 将测试用例编写为自注册函数(或方法,如果您愿意的话)。
  • 将测试用例划分为几个部分,每个部分都是独立运行的(消除了对夹具的需求)。
  • 使用BDD样式的“时准时限”部分以及传统的单元测试用例。
  • 仅一个核心声明宏可以进行比较。 使用标准C / C ++运算符进行比较-但是完整的表达式已分解,并且记录了lhs和rhs值。
  • 测试使用自由格式的字符串命名-合法标识符中没有其他名称。

核心特征

  • 可以对测试进行标记,以方便地运行临时的测试组。
  • 故障可能(可选)进入Windows和Mac上的调试器。
  • 输出通过模块化报告器对象。 包括基本的文本和XML报告程序。 自定义记者可以轻松添加。
  • 支持JUnit xml输出以与第三方工具(例如CI服务器)集成。
  • 提供了默认的main()函数,但您可以提供自己的控件来进行完全控制(例如,集成到自己的测试运行器GUI中)。
  • 提供了命令行解析器,如果您选择提供自己的main()函数,该解析器仍可以使用。
  • Catch可以测试自己。
  • 替代性断言宏报告失败,但不中止测试用例
  • 浮点公差比较是使用表达性的Approx()语法构建的。
  • 内部和友好的宏是隔离的,因此可以管理名称冲突
  • 匹配器

Catch示例

使用Catch进行单元测试,只需简单掌握TEST_CASE、REQUIRE、SECTION三个宏即可编写绝大部分的测试用例。简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
TEST_CASE( "vectors can be sized and resized", "[vector]" ) {

std::vector<int> v( 5 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );

SECTION( "resizing bigger changes size and capacity" ) {
v.resize( 10 );

REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "resizing smaller changes size but not capacity" ) {
v.resize( 0 );

REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
SECTION( "reserving bigger changes capacity but not size" ) {
v.reserve( 10 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "reserving smaller does not change size or capacity" ) {
v.reserve( 0 );

REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}

上述示例中,对于每个SECTION,TEST_CASE都是从头开始执行的,因此,当我们进入每个部分时,我们知道vector的大小为5,容量至少为5。通过REQUIRE宏在顶层确保vector大小和容量的正确性。这是可行的,因为SECTION宏包含一个if语句,该语句回调Catch来查看是否应执行该节。 通过TEST_CASE,每次运行都会执行一个叶子部分。 其他部分将被跳过。 下次执行下一个部分,依此类推,直到没有新的部分为止。

参考链接

  1. Writing Unit Tests with Catch and CMake,by filebox.
  2. Integrating catch2 with CMake and Jenkins,by mariuselvert.
  3. Catch2,by catchorg.
  4. C++单元测试入门,by pezy.
  5. Catch2 - 用于 test 的轻量级库,by Bluemultipl.

四元数与旋转矩阵

发表于 2020-01-15 | 更新于 2021-04-09

四元数是由爱尔兰数学家威廉·卢云·哈密顿在1843年创立出的数学概念。单位四元数(Unit quaternion)可以用于表示三维空间里的旋转。它与常用的另外两种表示方式(三维正交矩阵和欧拉角)是等价的,但是避免了欧拉角表示法中的万向锁问题。比起三维正交矩阵表示,四元数表示能够更方便地给出旋转的转轴与旋转角。

欧拉角

欧拉角(Euler Angles)是一种描述三维旋转的方式,根据欧拉旋转定理,任何一个旋转都可以用三个旋转的参数来表示。但欧拉角的描述方式有很多种,并没有一个统一标准。对于定义一个欧拉角,需要明确的内容包括:

  1. 三个旋转角的组合方式(是xyz还是yzx还是zxy)
  2. 旋转角度的参考坐标系统(旋转是相对于固定的坐标系还是相对于自身的坐标系)
  3. 使用旋转角度是左手系还是右手系
  4. 三个旋转角的记法

旋转角的记法

顺序 飞行器 望远镜 符号 角速度
第一 heading azimuth $θ$ yaw
第二 attitude elevation $ϕ$ pitch
第三 bank tilt $ψ$ roll

旋转矩阵

对于两个三维点 $p_1(x_1, y_1, z_1)$,$p_2(x_2,y_2,z_2)$,由点 $p_1$ 经过旋转矩阵 $R$ 旋转到 $p_2$,则有:

$$R = \left[ \begin{matrix}
r_{11} & r_{12} & r_{13}\
r_{21} & r_{22} & r_{23}\
r_{31} & r_{32} & r_{33}
\end{matrix} \right]$$

$$\left[ \begin{matrix}
x_2 \
y_2 \
z_2
\end{matrix} \right] = R
\left[ \begin{matrix}
x_1 \
y_1 \
z_1
\end{matrix} \right]$$

注:旋转矩阵为正交矩阵$RR^T=E$

  • 绕x轴旋转:

$$R_x(\theta) = \left[ \begin{matrix}
1 & 0 & 0\
0 & cos\theta & -sin\theta\
0 & sin\theta & cos\theta
\end{matrix} \right]$$

  • 绕y轴旋转:

$$R_y(\theta) = \left[ \begin{matrix}
cos\theta & 0 & sin\theta\
0 & 1 & 0\
-sin\theta & 0 & cos\theta
\end{matrix} \right]$$

  • 绕z轴旋转:

$$R_z(\theta) = \left[ \begin{matrix}
cos\theta & -sin\theta & 0\
sin\theta & cos\theta & 0\
0 & 0 & 1
\end{matrix} \right]$$

  • 任意旋转矩阵:

任何一个旋转可以表示为依次绕着三个旋转轴旋三个角度的组合。这三个角度称为欧拉角。

由欧拉角求旋转矩阵

设三个轴$x,y,z$的欧拉角分别为$θ_x,θ_y,θ_z$,正弦值、余弦值分别为$s_x, c_x, s_y, c_y, s_z, c_z$那么旋转矩阵为:

$$R(\theta_x,\theta_y,\theta_z)=R_z(\theta_z)R_y(\theta_y)R_x(\theta_x) = \left[ \begin{matrix}
c_y c_z & c_z s_x s_y - c_x s_z & s_x s_z + c_x c_z s_y\
c_y s_z & c_x c_z + s_x s_y s_z & c_x s_y s_z - c_z s_z\
-s_y & c_y s_x & c_x c_y
\end{matrix} \right]$$

由旋转矩阵求欧拉角

$$R = \left[ \begin{matrix}
r_{11} & r_{12} & r_{13}\
r_{21} & r_{22} & r_{23}\
r_{31} & r_{32} & r_{33}
\end{matrix} \right] = \left[ \begin{matrix}
c_y c_z & c_z s_x s_y - c_x s_z & s_x s_z + c_x c_z s_y\
c_y s_z & c_x c_z + s_x s_y s_z & c_x s_y s_z - c_z s_z\
-s_y & c_y s_x & c_x c_y
\end{matrix} \right]$$

解方程可得:

$$\theta_x = atan2(r_{32}, r_{33})$$
$$\theta_y = atan2(-r_{31}, \sqrt{r_{32}^2+r_{33}^2})$$
$$\theta_z = atan2(r_{21}, r_{11})$$

注意:atan2()为C++中的函数,atan2(y,x) 求的是y/x的反正切,其返回值为[-pi,+pi]之间的一个数。

四元数

三维空间的任意旋转,都可以用绕三维空间的某个轴旋转过某个角度来表示,即所谓的Axis-Angle表示方法。这种表示方法里,Axis可用一个三维向量$(x,y,z)$来表示,$θ$可以用一个角度值来表示,直观来讲,一个四维向量$(θ,x,y,z)$就可以表示出三维空间任意的旋转。注意,这里的三维向量$(x,y,z)$只是用来表示axis的方向朝向,因此更紧凑的表示方式是用一个单位向量来表示方向axis,而用该三维向量的长度来表示角度值$θ$。这样以来,可以用一个三维向量$(θ∗x,θ∗y,θ∗z)$就可以表示出三维空间任意的旋转,前提是其中$(x,y,z)$是单位向量。这就是旋转向量(Rotation Vector)的表示方式,OpenCV里大量使用的就是这种表示方法来表示旋转(见OpenCV相机标定部分的rvec)。

  • 单位向量(x,y,z)旋转θ角度后的四元数:

$$(cos \frac{\theta}{2}, xsin \frac{\theta}{2}, ysin \frac{\theta}{2}, z*sin \frac{\theta}{2})$$

  • 对于三维坐标的旋转,可以通过四元数乘法直接操作,与旋转矩阵操作可以等价,但是表示方式更加紧凑,计算量也可以小一些。

四元数求旋转矩阵

已知四元数:

$$\mathbf{q} = q_0 + q_1 i + q_2 j + q_3 k = [s, \mathbf{v}]$$

利用Rodrigues公式可以由四元数求得旋转矩阵R:

$$R = \left[ \begin{matrix}
1 - 2 q_2^2 - 2 q_3^2 & 2q_1 q_2 - 2q_0 q_3 & 2 q_1 q_3 + 2 q_0 q_2 \
2q_1 q_2 + 2q_0 q_3 & 1 - 2 q_1^2 - 2 q_3^2 & 2 q_2 q_3 - 2 q_0 q_1 \
2 q_1 q_3 - 2 q_0 q_2 & 2 q_2 q_3 + 2 q_0 q_1 & 1 - 2 q_1^2 - 2 q_2^2
\end{matrix} \right ]$$

旋转矩阵求四元数

给出其中一种情况的计算方法:

$$q_0 = \frac{\sqrt{1+r_{11}+r_{22}+r_{33}}}{2}$$
$$q_1 = \frac{r_{32}-r_{23}}{4q_0}$$
$$q_2 = \frac{r_{13}-r_{31}}{4q_0}$$
$$q_3 = \frac{r_{21}-r_{12}}{4q_0}$$

其中要满足 $q_0 \neq 0$,$1+r_{11}+r_{22}+r_{33}>0$,即 $1+tr(R)>0$

四元数进行姿态变换

假设坐标系O1上的点P1(x1, y1, z1), 存在变换矩阵R, 可计算P1点在坐标系O2上的坐标值为P2(x2, y2, z2):
$$P2=R∗P1$$

矩阵R对应的四元数为q, 则使用四元数计算为:
首先三维空间点用一个虚四元数来描述:P1=[0, x1, y1, z1], P2=[0, x2, y2, z2]
则P2和P1将计算关系为:

$$P_2=qP_1q^{-1}$$

参考链接

  1. 四元数,by wikipedia.
  2. 四元数与空间旋转,by wikipedia.
  3. 从旋转矩阵计算欧拉角,by PengChao.
  4. 旋转变换(一)旋转矩阵,by csxiaoshui.
  5. 旋转矩阵、欧拉角、四元数理论及其转换关系,by jason_ql.
  6. 旋转变换(二)欧拉角,by csxiaoshui.
  7. 欧拉角,by wikipedia.
  8. 欧拉角细节/旋转顺序/内旋外旋,by 能儿.
  9. 四元数运算与姿态变换,by Yoyo_wym.
  10. Flight Parameters and Quaternion Math,by mathwork.

JSBSim源代码分析

发表于 2020-01-13

JSBSim是一个开源跨平台的飞行动力学模型(FDM)软件库,用于模拟航空航天飞行器的飞行动力学。 该库已被纳入飞行模拟软件包FlightGear和OpenEaagles。JSBSim可以独立运行,通过命令行参数指定飞行器和初始状态,进行简单情境下的飞行动力学仿真,也可以将JSBSim作为代码库,编程实现飞行器模型加载,设置输入,获得输出。下面将通过分析JSBSim源代码,研究其实现通用飞行动力学模型的方法。

入口分析

下面是JSBSim参考手册中的最简单实例,因JSBSim的不断开发,JSBSim参考手册中该编程实例有点过时,因此进行了少量修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <FGFDMExec.h>
#include <sg_path.hxx>

using namespace std;

int main(int argc, char **argv)
{
JSBSim::FGFDMExec FDMExec;
bool result = true;

FDMExec.LoadScript(SGPath::fromUtf8(argv[1]));

while (result) result = FDMExec.Run();
}

从上述代码可知,调用JSBSim的主要方法是利用FGFDMExec类,通过实例化一个FGFDMExec类,就相当于获得了一个运行JSBSim仿真的工具箱,通过这个工具箱就可以调用JSBSim的大部分功能,实现我们要的仿真目标。同时FGFDMExec类通过加载外部飞机的XML脚本,实现飞行动力学模型的通用性。

JSBSim初始化流程

上述JSBSim最简仿真示例中已包含其初始化流程,采用图示如下:

JSBSim初始化流程

图1 JSBSim初始化流程

FGFDMExec初始化

FGFDMExec类在其构造函数中对各个模型进行初始化,具体代码在Allocate函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
FGFDMExec::FGFDMExec(FGPropertyManager* root, unsigned int* fdmctr)
: Root(root), RandomEngine(new default_random_engine), FDMctr(fdmctr)
{
...
try {
Allocate();
} catch (const string& msg ) {
cout << "Caught error: " << msg << endl;
exit(1);
}

...
}

Allocate函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
bool FGFDMExec::Allocate(void)
{
bool result=true;

Models.resize(eNumStandardModels);

// First build the inertial model since some other models are relying on
// the inertial model and the ground callback to build themselves.
// Note that this does not affect the order in which the models will be
// executed later.
Models[eInertial] = new FGInertial(this);

// See the eModels enum specification in the header file. The order of the
// enums specifies the order of execution. The Models[] vector is the primary
// storage array for the list of models.
Models[ePropagate] = new FGPropagate(this);
Models[eInput] = new FGInput(this);
Models[eAtmosphere] = new FGStandardAtmosphere(this);
...

// Assign the Model shortcuts for internal executive use only.
Propagate = (FGPropagate*)Models[ePropagate];
Inertial = (FGInertial*)Models[eInertial];
Atmosphere = (FGAtmosphere*)Models[eAtmosphere];
...

// Initialize planet (environment) constants
LoadPlanetConstants();

// Initialize models
for (unsigned int i = 0; i < Models.size(); i++) {
// The Input/Output models must not be initialized prior to IC loading
if (i == eInput || i == eOutput) continue;

LoadInputs(i);
Models[i]->InitModel();
}

...
return result;
}

Allocate函数代码中需要注意LoadInputs函数,该函数决定各个子模型的初始化顺序,确定各个子模型的输入输出,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void FGFDMExec::LoadInputs(unsigned int idx)
{
switch(idx) {
case ePropagate:
Propagate->in.vPQRidot = Accelerations->GetPQRidot();
Propagate->in.vUVWidot = Accelerations->GetUVWidot();
Propagate->in.DeltaT = dT;
break;
case eInput:
break;
case eInertial:
Inertial->in.Position = Propagate->GetLocation();
break;
case eAtmosphere:
Atmosphere->in.altitudeASL = Propagate->GetAltitudeASL();
break;
case eWinds:
Winds->in.AltitudeASL = Propagate->GetAltitudeASL();
Winds->in.DistanceAGL = Propagate->GetDistanceAGL();
...
break;
case eAuxiliary:
Auxiliary->in.Pressure = Atmosphere->GetPressure();
Auxiliary->in.Density = Atmosphere->GetDensity();
...
break;
case eSystems:
// Dynamic inputs come into the components that FCS manages through properties
break;
case ePropulsion:
Propulsion->in.Pressure = Atmosphere->GetPressure();
...

break;
case eAerodynamics:
Aerodynamics->in.Alpha = Auxiliary->Getalpha();
...
break;
case eGroundReactions:
// There are no external inputs to this model.
GroundReactions->in.Vground = Auxiliary->GetVground();
...
break;
case eExternalReactions:
// There are no external inputs to this model.
break;
case eBuoyantForces:
BuoyantForces->in.Density = Atmosphere->GetDensity();
...
break;
case eMassBalance:
MassBalance->in.GasInertia = BuoyantForces->GetGasMassInertia();
MassBalance->in.GasMass = BuoyantForces->GetGasMass();
...
break;
case eAircraft:
Aircraft->in.AeroForce = Aerodynamics->GetForces();
Aircraft->in.PropForce = Propulsion->GetForces();
...
break;
case eAccelerations:
Accelerations->in.J = MassBalance->GetJ();
Accelerations->in.Jinv = MassBalance->GetJinv();
...
break;
default:
break;
}
}

FGFDMExec加载飞机配置

FGFDMExec的LoadScript函数在初始化时负责加载飞机配置,用于初始化各个子模型。

1
2
3
4
5
6
7
8
9
10
bool FGFDMExec::LoadScript(const SGPath& script, double deltaT,
const SGPath& initfile)
{
bool result;

Script = new FGScript(this);
result = Script->LoadScript(GetFullPath(script), deltaT, initfile);

return result;
}

FGFDMExec运行

FGFDMExec的Run函数负责飞行动力学模型的计算,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool FGFDMExec::Run(void)
{
bool success=true;

...

// returns true if success, false if complete
if (Script != 0 && !IntegrationSuspended()) success = Script->RunScript();

for (unsigned int i = 0; i < Models.size(); i++) {
LoadInputs(i);
Models[i]->Run(holding);
}

...

return success;
}

FGFDMExec的Run函数将依次调用各个子模型的Run函数。

参考链接

  1. JSBSim编程实践之入门,by jackhuang.

排序算法总结

发表于 2020-01-05 | 更新于 2020-05-23

排序算法是计算机科学的基石之一,可从时间复杂度、空间复杂度、稳定性、是否原地排序等维度对排序算法进行分类。下面从时间复杂度方面对排序算法进行分类。

O(n^2)算法

冒泡排序

选择排序

插入排序

O(nlogn)算法

希尔排序

快速排序

归并排序

堆排序

O(n)算法

计数排序

桶排序

基数排序

参考链接

  1. 漫画:“排序算法” 大总结,by 小灰.
  2. 分布式哈希表 (DHT) 和 P2P 技术,by luyuhuang.
  3. Gzip 格式和 DEFLATE 压缩算法,by Luyu Huang.

Pandas入门教程

发表于 2020-01-02 | 更新于 2024-11-18

Pandas是一个开源的,BSD许可的库,为Python编程语言提供高性能,易于使用的数据结构和数据分析工具。

Pandas特色

Pandas 适用于处理以下类型的数据:

  • 与 SQL 或 Excel 表类似的,含异构列的表格数据;
  • 有序和无序(非固定频率)的时间序列数据;
  • 带行列标签的矩阵数据,包括同构或异构型数据;
  • 任意其它形式的观测、统计数据集, 数据转入 Pandas 数据结构时不必事先标记。

Pandas数据结构

Pandas 的主要数据结构是 Series(一维数据)与 DataFrame(二维数据),这两种数据结构足以处理金融、统计、社会科学、工程等领域里的大多数典型用例。对于 R 用户,DataFrame 提供了比 R 语言 data.frame 更丰富的功能。Pandas 基于 NumPy 开发,可以与其它第三方科学计算支持库完美集成。

维数 名称 描述
1 Series 带标签的一维同构数组
2 DataFrame 带标签的,大小可变的,二维异构表格

Pandas用法

Pandas用法与Matlab中矩阵操作很类似,熟悉Matlab操作的同学可以很快上手Pandas。

生成对象

生成Series对象:

1
2
3
4
5
6
7
8
9
10
11
In [3]: s = pd.Series([1, 3, 5, np.nan, 6, 8])

In [4]: s
Out[4]:
0 1.0
1 3.0
2 5.0
3 NaN
4 6.0
5 8.0
dtype: float64

生成DataFrame对象:

1
2
3
4
5
6
7
8
9
10
11
In [7]: df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))

In [8]: df
Out[8]:
A B C D
2013-01-01 0.469112 -0.282863 -1.509059 -1.135632
2013-01-02 1.212112 -0.173215 0.119209 -1.044236
2013-01-03 -0.861849 -2.104569 -0.494929 1.071804
2013-01-04 0.721555 -0.706771 -1.039575 0.271860
2013-01-05 -0.424972 0.567020 0.276232 -1.087401
2013-01-06 -0.673690 0.113648 -1.478427 0.524988

列切片

1
df1 = df[df.columns[0:6]];

重命名列名

1
df1.columns['id','name','sex','age','department','work']

过滤行

1
df1 = df1[df1.iloc[:,1]=='YES']

合并表格

pandas 2.0实现数据的合并与拼接,主要有三种方法:

  • join 最简单,主要用于基于索引的横向合并拼接
  • merge 最常用,主要用于基于指定列的横向合并拼接
  • concat最强大,可用于横向和纵向合并拼接
1
2
# 合并两个表,df1 和 df2 表结构一样
df3 = pd.concat([df1,df2])

毫秒解析

1
2
timeDelta = datetime.datetime(2024,11,11) - datetime.datetime(1900,1,1)
df3['datetime'] = pd.to_datetime(df['datetime'], format='%Y-%m-%d %H:%M:%S:%f') + timeDelta

按时间排序

1
df3 = df3.sort_values(by="datetime")

输出CSV表格

1
df3.to_csv(filePath + 'test.csv',index=0)

参考链接

  1. Pandas 中文,by pypandas.
  2. 十分钟入门 Pandas,by pypandas.
  3. Python读取csv文件的三种方式,by 涛声依旧2019.
  4. Python模块化开发组织代码程序示例,by BabyFish13.
  5. Python最佳实践指南!,by Prodesire.
  6. pandas中DataFrame 数据合并,连接(merge,join,concat) ,by Vincent-yuan.
  7. 【已解决】AttributeError: ‘DataFrame‘ object has no attribute ‘append‘,by zhtstar.
  8. pandas DataFrame数据重命名列名的几种方式,by littleRpl.
  9. Pandas 毫秒级时间解析,by 一定波兮.

jupyter notebook安装与使用

发表于 2019-12-31 | 更新于 2025-06-25

Jupyter Notebook(前身是IPython Notebook)是一个基于Web的交互式计算环境,用于创建Jupyter Notebook文档。Notebook一词可以通俗地引用许多不同的实体,主要是Jupyter Web应用程序、Jupyter Python Web服务器或Jupyter文档格式(取决于上下文)。Jupyter Notebook文档是一个JSON文档,遵循版本化模式,包含一个有序的输入/输出单元格列表,这些单元格可以包含代码、文本(使用Markdown语言)、数学、图表和富媒体,通常以“.ipynb”结尾扩展。

安装过程

安装前提

  • python>3.3 或者python=2.7

安装步骤

1
pip install notebook

启动Jupyter Notebook

1
jupyter notebook

使用指南

  • 单元格执行状态

单元格的执行状态对于复杂度高的代码,往往会意味着更长的执行等待时间。在Jupyter Notebook 中,当一个单元格处于执行状态时,单元格前面会出现 In [*] 符号,只有执行完成的单元格, [] 中的 * 才会变成相应的 序号。

除此之外,你可以通过页面右上角的 Kernel 状态指示器判断内核占用情况。如果 Python 字符右边出现了实心圆圈 ◉,代表内核处于占有状态。而空心圆圈 ◯ 则代表内核处于空闲状态。当然,也可能出现链接断开的符号,那就代表着内核已经断开链接,你可能需要刷新页面或重启实验环境。

参考链接

  1. Jupyter,by wikipedia.
  2. Installing the Jupyter Software,by jupyter.
  3. Matplotlib animation not working in IPython Notebook (blank plot),by stackoverflow.
  4. Jupyter Notebook使用指南,by zhanlang619.
  5. Jupyter中markdown的操作技巧,by 那一年_我九岁.

JavaScript正则表达式入门

发表于 2019-12-30 | 更新于 2019-12-31

最近在学习逐行剖析 Vue.js 源码的时候,发现Vuejs在模板编译时大量使用正则表达式。因此,将正则表达式的知识再温习一下。

基本概念

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、matchAll、replace、search 和 split 方法。

基本语法

特殊字符

正则表达式中的特殊字符:

\

依照下列规则匹配:

在非特殊字符之前的反斜杠表示下一个字符是特殊字符,不能按照字面理解。例如,前面没有 “" 的 “b” 通常匹配小写字母 “b”,即字符会被作为字面理解,无论它出现在哪里。但如果前面加了 “",它将不再匹配任何字符,而是表示一个字符边界。

在特殊字符之前的反斜杠表示下一个字符不是特殊字符,应该按照字面理解。详情请参阅下文中的 “转义(Escaping)” 部分。

如果你想将字符串传递给 RegExp 构造函数,不要忘记在字符串字面量中反斜杠是转义字符。所以为了在模式中添加一个反斜杠,你需要在字符串字面量中转义它。/[a-z]\s/i 和 new RegExp(“[a-z]\s”, “i”) 创建了相同的正则表达式:一个用于搜索后面紧跟着空白字符(\s 可看后文)并且在 a-z 范围内的任意字符的表达式。为了通过字符串字面量给 RegExp 构造函数创建包含反斜杠的表达式,你需要在字符串级别和表达式级别都对它进行转义。例如 /[a-z]:\/i 和 new RegExp(“[a-z]:\\“,”i”) 会创建相同的表达式,即匹配类似 “C:" 字符串。

^

匹配输入的开始。如果多行标志被设置为 true,那么也匹配换行符后紧跟的位置。

例如,/^A/ 并不会匹配 “an A” 中的 ‘A’,但是会匹配 “An E” 中的 ‘A’。

当 ‘^’ 作为第一个字符出现在一个字符集合模式时,它将会有不同的含义。反向字符集合 一节有详细介绍和示例。

$

匹配输入的结束。如果多行标示被设置为 true,那么也匹配换行符前的位置。

例如,/t$/ 并不会匹配 “eater” 中的 ‘t’,但是会匹配 “eat” 中的 ‘t’。

*

匹配前一个表达式 0 次或多次。等价于 {0,}。

例如,/bo*/ 会匹配 “A ghost boooooed” 中的 ‘booooo’ 和 “A bird warbled” 中的 ‘b’,但是在 “A goat grunted” 中不会匹配任何内容。

+

匹配前面一个表达式 1 次或者多次。等价于 {1,}。

例如,/a+/ 会匹配 “candy” 中的 ‘a’ 和 “caaaaaaandy” 中所有的 ‘a’,但是在 “cndy” 中不会匹配任何内容。

?

匹配前面一个表达式 0 次或者 1 次。等价于 {0,1}。

例如,/e?le?/ 匹配 “angel” 中的 ‘el’、”angle” 中的 ‘le’ 以及 “oslo’ 中的 ‘l’。

如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。例如,对 “123abc” 使用 /\d+/ 将会匹配 “123”,而使用 /\d+?/ 则只会匹配到 “1”。

还用于先行断言中,如本表的 x(?=y) 和 x(?!y) 条目所述。

.

小数点)默认匹配除换行符之外的任何单个字符。

例如,/.n/ 将会匹配 “nay, an apple is on the tree” 中的 ‘an’ 和 ‘on’,但是不会匹配 ‘nay’。

如果 s (“dotAll”) 标志位被设为 true,它也会匹配换行符。

\n

在正则表达式中,它返回最后的第n个子捕获匹配的子字符串(捕获的数目以左括号计数)。

比如 /apple(,)\sorange\1/ 匹配”apple, orange, cherry, peach.”中的’apple, orange,’ 。

\s

匹配一个空白字符,包括空格、制表符、换页符和换行符。等价于[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。

例如, /\s\w*/ 匹配”foo bar.”中的’ bar’。

\w

匹配一个单字字符(字母、数字或者下划线)。等价于 [A-Za-z0-9_]。

例如, /\w/ 匹配 “apple,” 中的 ‘a’,”$5.28,”中的 ‘5’ 和 “3D.” 中的 ‘3’。

\W

匹配一个非单字字符。等价于 [^A-Za-z0-9_]。

例如, /\W/ 或者 /[^A-Za-z0-9_]/ 匹配 “50%.” 中的 ‘%’。

(x)

像下面的例子展示的那样,它会匹配 ‘x’ 并且记住匹配项。其中括号被称为捕获括号。

模式 /(foo) (bar) \1 \2/ 中的 ‘(foo)’ 和 ‘(bar)’ 匹配并记住字符串 “foo bar foo bar” 中前两个单词。模式中的 \1 和 \2 表示第一个和第二个被捕获括号匹配的子字符串,即 foo 和 bar,匹配了原字符串中的后两个单词。注意 \1、\2、…、\n 是用在正则表达式的匹配环节,详情可以参阅后文的 \n 条目。而在正则表达式的替换环节,则要使用像 $1、$2、…、$n 这样的语法,例如,’bar foo’.replace(/(…) (…)/, ‘$2 $1’)。$& 表示整个用于匹配的原字符串。

标志

标志 描述
g 全局搜索。
i 不区分大小写搜索。
m 多行搜索。
s 允许 . 匹配换行符。
u 使用unicode码的模式进行匹配。
y 执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标

使用方法

正则表达式可以被用于 RegExp 的 exec 和 test 方法以及 String 的 match、replace、search 和 split 方法。使用正则表达式的方法如下:

方法

方法 描述
exec 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。
test 一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。
match 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。
matchAll 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。
search 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
replace 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
split 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

返回

对象 属性或索引 描述 在例子中对应的值
myArray 匹配到的字符串和所有被记住的子字符串。 [“dbbd”, “bb”]
myArray index 在输入的字符串中匹配到的以0开始的索引值。 1
myArray input 初始字符串。 “cdbbdbsbz”
myArray [0] 匹配到的所有字符串(并不是匹配后记住的字符串)。注:原文”The last matched characters.”,应该是原版错误。匹配到的最终字符。 “dbbd”
myRe lastIndex 下一个匹配的索引值。(这个属性只有在使用g参数时可用在 通过参数进行高级搜索 一节有详细的描述.) 5
myRe source 模式文本。在正则表达式创建时更新,不执行。 “d(b+)d”

示例

1
2
3
4
5
6
var re = /\w+\s/g;
var str = "fee fi fo fum";
var myArray = str.match(re);
console.log(myArray);

// ["fee ", "fi ", "fo "]

参考链接

  1. 逐行剖析 Vue.js 源码,by nlrx.
  2. 正则表达式,by mozilla.

编译原理学习笔记

发表于 2019-12-29

编译原理是计算机专业的一门重要专业课,旨在介绍编译程序构造的一般原理和基本方法。内容包括语言和文法、词法分析、语法分析、语法制导翻译、中间代码生成、存储管理、代码优化和目标代码生成。

基本概念

  • 词法分析

从左到右逐个字符地扫描,从中识别出一个个“单词”符号。“单词”符号是程序设计语言的基本语法单位,如关键字、标识符、常数、运算符和分隔符等。

  • 语法分析

根据语言的语法规则将单词符号序列分解成各类语法单位,比如表达式、语句和程序等。语法规则就是各类语法单位的构成规则。通过语法分析确定整个输入串是否构成一个语法上正确的程序。

  • 语义分析

检查源程序是否包含静态语义错误,并收集类型信息供后面的代码生成阶段使用。只有语法和语义都正确的源程序才能被翻译成正确的目标代码。

语义分析的一个主要工作是进行类型分析和检查。程序语言中的一个数据类型一般包含两个方面的内容:类型的载体及其上的运算。例如:整除取余运算只能对整型数据进行运算,若其运算对象中有浮点数就认为是类型不匹配的错误。静态的语义错误是指编译程序可以发现,动态的语义错误是指源程序虽然能够被编译和执行,但是结果不对,一般是逻辑上的错误。

编译的过程

编译程序的工作过程一般可以分为5个阶段:

  1. 词法分析
  2. 语法分析
  3. 语义分析和中间代码的产生
  4. 优化
  5. 目标代码生成

参考链接

  1. AST 抽象语法树,by Jartto.
  2. 【编译原理】编译原理简单介绍,by cflys.
  3. 编译原理,by junhey.

3D模型动画分类及其使用

发表于 2019-12-28

3DMax、Blender之类的3D建模软件易学难精,其原因在于很多人不了解其背后的计算机图形学原理。因此,掌握相关的计算机图形学原理和知识,对于我们熟练运用3D建模软件是十分必要的。下面简单介绍3D模型的分类及其使用方法。

3D模型动画分类

3D模型动画的基本原理是让模型中各顶点的位置随时间变化。 主要种类有Morph(变形)动画,关节动画和骨骼蒙皮动画(SkinnedMesh)。从动画数据的角度来说,三者一般都采用关键帧技术,即只给出关键帧的数据,其他帧的数据使用插值得到。但由于这三种技术的不同,关键帧的数据是不一样的。

变形动画

Morph(渐变,变形)动画是直接指定动画每一帧的顶点位置,其动画关键中存储的是Mesh所有顶点在关键帧对应时刻的位置。

关节动画

关节动画的模型不是一个整体的Mesh,而是分成很多部分(Mesh),通过一个父子层次结构将这些分散的Mesh组织在一起,父Mesh带动其下子Mesh的运动,各Mesh中的顶点坐标定义在自己的坐标系中,这样各个Mesh是作为一个整体参与运动的。

动画帧中设置各子Mesh相对于其父Mesh的变换(主要是旋转,当然也可包括移动和缩放),通过子到父,一级级的变换累加(当然从技术上,如果是矩阵操作是累乘)得到该Mesh在整个动画模型所在的坐标空间中的变换(从本文的视角来说就是世界坐标系了,下同),从而确定每个Mesh在世界坐标系中的位置和方向,然后以Mesh为单位渲染即可。

关节动画的问题是,各部分Mesh中的顶点是固定在其Mesh坐标系中的,这样在两个Mesh结合处就可能产生裂缝。

骨骼蒙皮动画

骨骼蒙皮动画即SkinnedMesh了,骨骼蒙皮动画的出现解决了关节动画的裂缝问题。骨骼动画的基本原理可概括为:在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。

一个骨骼动画通常包括骨骼层次结构数据,网格(Mesh)数据,网格蒙皮数据(skin info)和骨骼的动画(关键帧)数据。

SkinnedMesh原理

SkinnedMesh中文一般称作骨骼蒙皮动画,正如其名,这种动画中包含骨骼(Bone)和蒙皮(Skinned Mesh)两个部分,Bone的层次结构和关节动画类似,Mesh则和关节动画不同:

关节动画中是使用多个分散的Mesh,而Skinned Mesh中Mesh是一个整体,也就是说只有一个Mesh,实际上如果没有骨骼让Mesh运动变形,Mesh就和静态模型一样了。

Skinned Mesh技术的精华在于蒙皮,所谓的皮并不是模型的贴图(也许会有人这么想过吧),而是Mesh本身,蒙皮是指将Mesh中的顶点附着(绑定)在骨骼之上,而且每个顶点可以被多个骨骼所控制,这样在关节处的顶点由于同时受到父子骨骼的拉扯而改变位置就消除了裂缝。

Skinned Mesh这个词从字面上理解似乎是有皮的模型,哦,如果贴图是皮,那么普通静态模型不也都有吗?所以我觉得应该理解为具有蒙皮信息的Mesh或可当做皮肤用的Mesh,这个皮肤就是Mesh。而为了有皮肤功能,Mesh还需要蒙皮信息,即Skin数据,没有Skin数据就是一个普通的静态Mesh了。

Skin数据决定顶点如何绑定到骨骼上。顶点的Skin数据包括顶点受哪些骨骼影响以及这些骨骼影响该顶点时的权重(weight),另外对于每块骨骼还需要骨骼偏移矩阵(BoneOffsetMatrix)用来将顶点从Mesh空间变换到骨骼空间。

SkinnedMesh结构

  • 骨骼决定了模型整体在世界坐标系中的位置和朝向。

先看看静态模型吧,静态模型没有骨骼,我们在世界坐标系中放置静态模型时,只要指定模型自身坐标系在世界坐标系中的位置和朝向。在骨骼动画中,不是把Mesh直接放到世界坐标系中,Mesh只是作为Skin使用的,是依附于骨骼的,真正决定模型在世界坐标系中的位置和朝向的是骨骼。

在渲染静态模型时,由于模型的顶点都是定义在模型坐标系中的,所以各顶点只要经过模型坐标系到世界坐标系的变换后就可进行渲染。而对于骨骼动画,我们设置模型的位置和朝向,实际是在设置根骨骼的位置和朝向,然后根据骨骼层次结构中父子骨骼之间的变换关系计算出各个骨骼的位置和朝向,然后根据骨骼对Mesh中顶点的绑定计算出顶点在世界坐标系中的坐标,从而对顶点进行渲染。要记住,在骨骼动画中,骨骼才是模型主体,Mesh不过是一层皮,一件衣服。

  • 骨骼可理解为一个坐标空间。

骨骼只是一个形象的说法,实际上骨骼可理解为一个坐标空间,关节可理解为骨骼坐标空间的原点。关节的位置由它在父骨骼坐标空间中的位置描述。上图中有三块骨骼,分别是上臂,前臂和两个手指。Clavicle(锁骨)是一个关节,它是上臂的原点,同样肘关节(elbow joint)是前臂的原点,腕关节(wrist)是手指骨骼的原点。关节既决定了骨骼空间的位置,又是骨骼空间的旋转和缩放中心。

骨骼就是坐标空间,骨骼层次就是嵌套的坐标空间。关节只是描述骨骼的位置即骨骼自己的坐标空间原点在其父空间中的位置,绕关节旋转是指骨骼坐标空间(包括所有子空间)自身的旋转。

但还有两个可能的疑问,一是骨骼的长度问题,由于骨骼是坐标空间,没有所谓的长度和宽度的限制,我们看到的长度一方面是蒙皮后的结果,另一方面子骨骼的原点(也就是关节)的位置往往决定了视觉上父骨骼的长度,比如这里upper arm线段的长度实际是由elbow joint的位置决定的。

第二个问题,手指的那个端点是啥啊?实际上在我们的例子中手指没有子骨骼,所以那个端点并不存在:)那是为了方便演示画上去的。实际问题中总有最下层的骨骼,他们不能决定其他骨骼了,他们的作用只剩下控制Mesh顶点。对了,那么手指的长度如何确定?我们看到的长度应该是由蒙皮决定的,也就是由Mesh中属于手指的那些点离腕关节的距离决定。

3D模型动画使用

下面给出一段在Unity3D中控制3D模型动画的代码,作为参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

using UnityEngine;
using System.Collections;

public class AnimationScript : MonoBehaviour
{
void Start()
{
Animation animation = this.animation;//动画控制器
animation.Play("idle");//上来直接播放idle动画
}
void OnGUI()
{
if (GUI.Button(new Rect(0, 0, 100, 30), "行走"))
{
animation.Play("run");
}
if (GUI.Button(new Rect(100, 0, 100, 30), "停止"))
{
animation.Play("idle");
}
if (GUI.Button(new Rect(200, 0, 100, 30), "攻击"))
{
animation.Play("attack");
animation.PlayQueued("idle");//播放完attack之后再播放idle
}
}
}

参考链接

  1. 骨骼蒙皮动画(SkinnedMesh)的原理解析,by feng.
  2. 【Unity3D】3D模型的使用——FBX的使用与Animation设置,by yongh701.

GitBook入门教程

发表于 2019-12-26 | 更新于 2020-01-28

GitBook是一种制作在线书籍的工具。它基于Git支持多人协作,支持将采用Markdown语法编辑的文档导出成 PDF,EPUB,HTML等多种格式。

GitBook安装

环境要求

  • NodeJS (v4.0.0 and above is recommended)
  • Windows, Linux, Unix, or Mac OS X

NPM安装GitBook

通过NPM工具安装GitBook是最佳的方法:

1
2
$ npm install gitbook-cli -g
$ gitbook init //下载稳定版的gitbook,同时创建在线书籍

gitbook-cli工具可安装多个GitBook版本到系统上。对于Windows平台,gitbook-cli工具安装的多个GitBook版本通常存储在“C:\Users\CurrentLoginUser\.gitbook”。

离线安装GitBook

内网机器上安装GitBook的方法如下:

  • 安装最新Nodejs长期支持版。
  • 使用npm-bundle命令在线打包gitbook-cli
1
2
npm install npm-bundle -g
npm-bundle gitbook-cli
  • 内网机器上安装gitbook-cli
1
npm install ./gitbook-cli.tgz
  • 将“C:\Users\CurrentLoginUser\.gitbook”目录打包拷贝至内网机器对应位置

创建书籍

1
2
3
$ gitbook init    //在当前目录创建书籍
$ gitbook build //构建在线书籍网站
$ gitbook serve //构建在线书籍网站并启动

参考链接

  1. GitBook 从懵逼到入门,by 阿基米东.
  2. 使用 Gitbook 打造你的电子书,by 文艺小青年.
  3. 世上最佳离线markdown编辑工具(gitbook和gitbook editor),by icharm.
  4. 移除GitBook目录下方的“本书使用GitBook发布”字样,by tedxiong.
  5. EbookError: Error during ebook generation: ‘ebook-convert,by 狼爷.
  6. 书籍配置文件(book.json),by wiliam.
上一页1…303132…53下一页

Jack Huang

523 日志
67 标签
© 2025 Jack Huang
由 Hexo 强力驱动
|
主题 — NexT.Muse