|
3 k L' e6 \ v8 ^' N 2021.10.24更新:两个小Tips:1️⃣这个包我现在不用了,毕竟主力平台还是Win,现在推荐使用ProPlot来画图(ProPlot — ProPlot documentation),真的很好用(导师看了都直呼内行,开个玩笑)!2️⃣文中提到的EOF的代码就别看了(属于是早年的腊鸡代码),现在推荐直接使用eofs(Package description — eofs 1.4.0 documentation (ajdawson.github.io))来完成EOF的计算。
+ b- j/ C' m; F: i 2021.01.31更新: 1 j6 C* S; U" B
创建新环境时请将Python版本指定为3.6,否则会冲突导致无法安装。
7 U2 K+ l5 R& O; [& F 一点感想:PyNgl和PyNio也不再更新了,美帝又去更新自己的Geocat包了,可惜啊Geocat我自己初次尝试,连安装都很大困难,还是学学人家cartopy吧,要不是cartopy画图太丑。 % X/ f. F- T. f/ a. y( |& d
更新:
. n1 m2 W7 ~! a. m8 E9 q' f& c0 O conda create --name pyncl --channel conda-forge/label/cf201901 pynio pyngl netcdf4 xarray ncl scipy 8 N8 W' h. L% O1 u9 F4 e
此安装代码不建议在使用,速度慢,容易掉线。
( N. C f; w/ u& [% E) w) @, B 输入命令:vi ~/.condarc " C& `. ~' v1 ?" k" _2 p( }
讲最后一行的 -default 直接删除(前提是已经增加了清华或中科大源) 9 [2 p7 Y, I8 M- Y( r+ I% a5 k- U
然后在新环境下使用conda install xxx 依次安装pynio pyngl netcdf4 xarray ncl scipy即可快速安装 ! H3 [. }# X. y* q% \6 }3 p
如果换源之后还是不行
% j7 W/ Z: {- K8 c- ?) r( w: i 数据可视化的软件和库有很多,但是对于大气和海洋数据的可视化,因为涉及到投影、海岸线的绘制,支持的还非常少。 ( z3 t2 k/ F2 n* j" [4 v
主要的有: / J& r- j! K) }
①Grads:易用性很差,常常是数据处理计算出来再由Grads读取来绘制。同时语法也比较奇怪。对Win10的支持也不是很好。 ( Z& a3 h. Z' u- M
②NCL:NCL(The NCAR Command Language)是一种专门为大气海洋科学数据处理以及数据可视化设计的高级语言。其实是PyNgl和PyNio的前代产品。
4 J2 {% D0 O( e7 Q& |" X9 o ③MATLAB:很好用但是主攻方向不在地球科学,绘制出来的图片稍微欠缺没敢。(个人感觉)
- L, H# a' E+ q% R5 w ④Cartopy:Python下的一个库,由英国气象局开发,Basemap的继承产品(Basemap在2020年不再维护),主要基于matplotlib实现可视化。目前还不是很完善,容易出现奇奇怪怪的问题。 x; h8 T. D# d
⑤PyNgl:NCL的续作由NCAR负责开发。绘制美观,功能强大,绘图时允许高度自定义。放一张官网扒的图
, O% o! M# M. Z4 {
# C ^) w1 \& B; P0 N 还是挺美观的。
3 Y3 E% I1 [+ [" k6 p. g 为什么选择Python?因为 人生苦短,我用Python!!! 它语法简单高效,一般来说上手三天即可(但是精通永无止境),学习曲线很平滑。 1 I* i0 E0 _$ U+ H- p
既然PyNgl是Python下面的一个库,那么直接用conda安装不就完了吗,理论上来说是的,但是关键在于:
. T6 |9 k9 J! \* ?6 l, N5 o 它不支持Win10!!! 3 K5 {. b) |5 J. w4 q+ A4 c/ y
它不支持Win10!!! + H. z8 s5 l! ]+ ?+ ^
它不支持Win10!!!
) J6 a" I3 ~. H, x* | 这对于不常用Linux的科学工作者或者学生来说是很麻烦的事情,大多数人的主力系统都是Win10,当然macos系统也不需要参考此教程,可以直接利用conda安装。 6 k6 m7 }- n" Y5 q% T
幸运的是微软家有一个很好用的东西:子系统(WSL),Windows Subsystem for Linux(简称 WSL)是一个为在 Windows 10 上能够原生运行 Linux 二进制可执行文件(ELF 格式)的兼容层。它是由微软与 Canonical 公司合作开发,目标是使纯正的 Ubuntu 14.04 “Trusty Tahr”映像能下载和解压到用户的本地计算机,并且映像内的工具和实用工具能在此子系统上原生运行。
; [9 L6 d2 u; V4 O5 p" A3 _ 相比于 VMware 等虚拟机,WSL 占用内存和 CPU 资源更少,在 WSL 上运行软件的消耗和直接在 Windows 上差不多。而且,Windows 下可以直接访问 WSL 的环境。 ; K/ {& z+ o& X6 d; c$ ?0 X
安装子系统步骤:
3 `* \+ }: H" H, L) h5 L ①开启 WSL 可选特性
/ ]$ z! }' Z* o3 T0 y. h j1 h: N 在控制面板的“启动或关闭 Windows 功能”中勾选“适用于 Linux 的 Windows 子系统”。 % C/ w8 H# n5 T' B4 T! s
. v! x" M; N4 \, q# {! z% ^ 或在 PowerShell 中运行下述代码:
; e- Z/ v5 N* |7 I" p; M- m Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 6 B/ A) ~5 |/ M+ d
②下载安装
# d& }1 P' o; \1 _" } 打开 Microsoft Store,搜索 Linux,就会显示 Ubuntu、suse 等几个发行版,点击进行安装即可。这里选择了 Ubuntu(第一个)。 . J1 `9 N; h. E$ d' T' Q
; g; [/ r; I( Z4 l5 ^& s
下载之后启动菜单里就会出现Ubuntu的图标了。让我们启动它,按照上面的提示等待几分钟,就可以进入初次登陆设置账号的界面。
( ~: g: ~3 Z; F$ f2 m7 s" o 图片来自https://wu-kan.cn/_posts/2018-12-14-Windows-Subsystem-for-Linux/设置账号密码就完成了安装 8 S8 Q" }3 D- @* N% P1 u
图片来自https://wu-kan.cn/_posts/2018-12-14-Windows-Subsystem-for-Linux/关于Linux的命令可以自行查阅,只需要会cd,ls,pwd等简单命令即可。 : F; E, |2 Z5 P5 M, ?- P4 \+ P
需要注意:如无必要不要输入下面命令,因为更新后貌似会有些问题,导致PyNgl无法使用。 ' |9 \4 w3 ~/ J3 s
sudo apt update -y5 x. M) O8 N; t. m c5 _
sudo apt upgrade -y + r3 g8 R2 n: N
子系统下anaconda的安装: " A! z. `4 ~7 W' t
①首先从清华镜像Tsinghua Open Source Mirror下载ananconda在linux下的镜像,并放到Win10系统的任意位置(这里假设是D盘) . @# c6 F/ T8 c
. v9 R) ~# T; m 接着在子系统中输入下面命令 9 S# C0 ^1 R+ J8 c( v
bash /mnt/d/Anaconda3-2020.02-Linux-x86_64.sh#mnt其实是挂载到了本地的硬盘 + Z7 X8 g7 G% X9 w; }
一路回车,注意在正式安装前会弹出安装地址,记下这个位置。然后他会开始安装,等就行。
1 s* Y- z% |; N9 @+ b ②环境变量的设置(保证在子系统的任意位置均可使用Python和conda命令)
8 V7 j3 B5 V7 O& P. F 1,输入 vi ~/.bashrc (打开了一个编辑器)
" t$ Z, K- t0 w2 |5 j7 ? 2,按一下i键,进入编辑模式
5 V' E8 @! t$ K 3,在最后输入export PATH=/home/lishanliao/anaconda3/bin PATH 要换成你自己的目录,如果你是一路回车安装的一般只要把lishanliao换成你自己的用户名即可
) s" ]3 _8 N/ Q8 o" o 4,按下shift+ i旁边那个冒号键),输入wq 保存并退出。 # S, L$ U+ W# D( M/ \7 ?# d) R
5,输入source ~/.bashrc 重新加载环境变量 * ^, y0 k, f4 s, ^- R
6,如果你害怕不会用这个编辑器,可以直接在命令行中输入下面命令,在执行5即可
. ^6 m& D% o% m3 l+ j: b echo export PATH ="/home/liang_yu/anaconda3/bin  PATH" >> ~/.bashrc#还是要换成你自己的路径
$ e7 m! Y' Q' v) K" t/ P* V5 ^ ③conda下载源更换(加快库的下载速度)
! L# ^2 X4 ?! O0 L }$ y 直接在linux命令行中输入下面命令(复制粘贴即可) + O; z2 E4 D% Q
conda config --add channels https://mirrors.ustc.edu.cn/anaconda/pkgs/main/8 U# _$ {9 _% I D: f" d
conda config --add channels https://mirrors.ustc.edu.cn/anaconda/pkgs/free/
! }1 u2 P/ J2 o8 H conda config --add channels https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge/
+ E3 X3 p" ^& z8 F conda config --add channels https://mirrors.ustc.edu.cn/anaconda/cloud/msys2/: D/ Q3 B+ x+ P5 C) Q& p2 t+ g
conda config --add channels https://mirrors.ustc.edu.cn/anaconda/cloud/bioconda/
( U4 r' R4 w- x! P# ?; @ conda config --add channels https://mirrors.ustc.edu.cn/anaconda/cloud/menpo/
# a. m F6 d2 ^+ F9 r4 i conda config --set show_channel_urls yes . R* z& P) s4 }+ g& U. m2 F" j; }
④PyNgl的安装
3 R0 S; }; W2 E3 }5 F8 I* X4 S% ~ 在linux命令行中输入下面命令,他会自行安装,可能会出现网络错误,要么多试几遍要么挂梯子 ' A' t& W q0 y, c' p s
conda create --name pyncl --channel conda-forge/label/cf201901 pynio pyngl netcdf4 xarray ncl scipy - P# J; w, g# S+ }
我并没把库安装在base环境下,而是创建了一个新的环境叫pyncl,因为好像有某些冲突。 ) K" A$ _7 R. b/ p7 B
安装完成后,输入python,在输入 import Ngl,如果没有报错就是成功了。 6 Y; E: M5 N/ `
⑤接下来就可以画图啦
" ]) W/ Z( ^3 l5 M; n0 |; c* A 现在win10下写好代码,保存成.py文件
" {/ Z0 v6 w8 P1 \, q 首先激活环境,输入下面命令
- u8 b$ ~+ c7 `$ K/ V conda activate pyncl : n6 Q4 n6 ]0 _( n/ _4 k8 _3 Y
接着输入,它会自动执行。
, Q- c0 v* O( K' G2 \! x* Z python /mnt/d/file.py 9 G' l% E- Q+ r( R! `
最后展示一下示例图和代码 ; S5 H B7 Q5 y
- T2 G y( W5 s0 t( m$ [$ x( l 代码示例(前半部分是EOF的计算,可以不看) * Q5 C, f- q$ i3 N
"""实现了自由调整子图的位置,没有使用panel的面板绘图,使用了新的地图投影"""
- R! r/ u* R p# \- K# T, h import Ngl7 O+ E) m: Q+ Z/ B# ^& M
import numpy as np9 K/ l. r2 l5 K/ T% H' @
import math8 _. P9 u% L% | B
import netCDF4 as nc% ?2 J8 K( D0 t. G- N% H4 Z
def EOF_3d(data_3d,lat,lon,K):1 N- S/ d$ `7 D0 R5 [, K$ D- d8 A
"""data_3d表示三维数据
8 j: H$ v9 h7 O, ~4 p0 d 例如某一时间段,某一区域范围
0 h9 B, y, d! t/ G& H( n2 `1 Q7 o' j 的SST数据,需先做拉直运算
+ F/ d, l. t+ ~0 V1 ? K是int表示去前K个模态' |3 ?& i6 ]( ]4 l6 B0 N \1 D8 n9 @0 _3 e
同时考虑到时空转换K不能大于n
% A( r4 @9 e2 g/ L$ P5 C3 w0 I 因为L_Q的形状为[n,n],看表达式就可以看出来"""
( d" H4 h+ W9 R" [" Y lat_n,lon_n,time=data_3d.shape
2 G' {/ s* k5 S" N& x0 n m=lat_n*lon_n;n=time#m为个点数,n为观测次数) n1 E- r( e& k( R9 ?. F
m=m-np.count_nonzero(data_3d[:,:,0]!=data_3d[:,:,0])4 ]' G) D% ?; \
X=np.empty((m,n))
6 S( ?. \, m$ V7 i k=0
# z* a4 t2 W4 ~. t, H ?3 d" w for i in range(lat_n):
8 Q4 X& Y5 U$ Z4 o for j in range(lon_n):
! y$ M% G' i9 w5 A1 P6 B if (np.isnan(data_3d[i,j,0])):#考虑陆地
0 B6 ]6 A( O- s4 W$ j( r" c) l print (land)! T9 |# W) N& K5 H; E7 x7 M" G# E
else:
1 O8 y" q4 a" U: n* c. k1 z4 M #考虑地球因素乘于cos(纬度)
) S5 k* i5 M5 j7 ~ X[k,:]=data_3d[i,j,:]*(math.cos(math.pi*lat[i]/180))**0.5
6 K& d7 E# {. ?4 x% ? p k=k+1
, M/ E( T* W, I* g3 ?. z X_mean=X.mean(axis=1).reshape((m,1))5 U( f9 }0 ~- l' w1 H# s `+ w3 h
X_d=X-X_mean#利用距平阵计算
% Q- S5 _( Y( J9 t { H' e9 C if (m<=n):#不使用时空转换6 t( I, Y/ s j
S=np.dot(X_d,X_d.T)/n( s, j" S4 d2 @( E. S3 g
lambda_,L=np.linalg.eig(S)
3 B( J1 ]4 e* q) ]1 \' s# W else:
2 g/ S& e0 q, I5 y S_Q=np.dot(X_d.T,X_d)/m) c5 A+ H6 v P2 d; ~1 D
lambda_Q,L_Q=np.linalg.eig(S_Q)#注意L_Q的形状 w/ A1 Z4 Y |5 W
lambda_=lambda_Q*(m/n)
/ O, b1 @2 a; s: L* V, D L=np.empty((m,K))
1 ~) b4 c% i6 ?, Y* _1 z) | for i in range(K):
( \- W' C* T9 A' O/ E2 ]( U: K l_Q=L_Q.real[:,i]
; o( K9 D9 l, l( O3 M! B7 L5 [ L[:,i]=np.dot(X_d,l_Q.T)/(m*lambda_Q[i])**0.5. q' J0 I; w& ?0 n. y1 X, H7 C; i
lambda_=lambda_.real[0:K];L=L.real[:,0:K]#避免复数的问题6 E5 i" S" X. Y- B# J5 X" K
Y=np.dot(L.T,X_d)# m Y4 } A5 h4 T& {: x6 t
p=np.empty((K,n))8 }" O0 a2 v1 g$ V4 K0 @4 w! \
e=np.empty((m,K))2 ~7 W9 ]3 _! a7 o+ N# s' ]- g+ K
for i in range(K):
6 s" O4 r, s% g3 V9 s+ Y e[:,i]=lambda_[i]**0.5*L[:,i]
' Y# A) l' W# R p[i,:]=Y[i,:]/lambda_[i]**0.5 W" Y- f: F, Q) d+ O- U* r2 k
#将e恢复为空间性 i6 \. M& C/ l7 e
e_2d=np.empty((lat_n,lon_n,K))7 U$ P8 i8 I5 S# ~- E
for m in range(K):# q$ c$ @1 z/ _8 }5 ?, K
k=0" t2 G, f1 n. B' N" Z; E
for i in range(lat_n):- V- y: d7 }/ ]1 ?2 `' z
for j in range(lon_n):
. x1 a+ \( R; y* M! D if (np.isnan(data_3d[i,j,0])):
( ^! x. |, @% o' R e_2d[i,j,m]=np.nan
9 [3 {% Y+ c Q4 {% L2 m else:
* Z9 M' ^" u! i. T' i. T& K9 J e_2d[i,j,m]=e[k,m]/(math.cos(math.pi*lat[i]/180))**0.5
& [4 a, l' G" N" O( o k=k+17 f' t" W( i8 v5 \
return lambda_,e_2d,p,Y
1 ^6 x& }! B! v i2 B #------------------
' ^6 D2 D6 T4 N2 n #时间:2000-2018年每年1月(624开始19个月)5 j. p* O1 b1 o4 ~
#区域:0-85,120-245* N' g* T% b" m+ G* R# k% }% b2 b
#lat[2:37] 359 {2 [! M @6 V% l. D. B
#lon[48:99] 51
" w1 Y& Q- ?/ ^" Y F file="/mnt/c/Users/59799/Desktop/slp.mon.mean.nc"+ t! a2 G7 J" v' G+ f. Q3 J& W
#file=rC:\Users\59799\Desktop\slp.mon.mean.nc
* U# X. R% _! f data = nc.Dataset(file)
2 Q6 M; \+ t" J/ b( y2 m3 z lat = np.array(data.variables[lat])
: P4 {$ |' B0 }4 J5 q lon = np.array(data.variables[lon])' h5 k) X$ v0 v
time = np.array(data.variables[time])
! A+ L2 s+ w' V; z; [, D slp = np.array(data.variables[slp]) P! Z) e" u& B4 g4 r$ K! P! z* {% D: ]
data_3d=np.empty((35,51,19))
7 X3 t4 T8 O+ i* |2 k, W. W time=624
! B( n; s7 A1 [* J7 f for k in range(19):
' V# y2 S* o& b for i in range(35):3 Z4 J5 P) `. p) V$ c' h
for j in range(51):' H# ~9 _/ X; l# j m0 k
data_3d[i,j,k]=slp[time,2+i,48+j]
( v1 l0 M0 D7 Q. U" k time=time+12
+ i6 S/ X. B( ?' R6 B7 q lad,e_2d,p_t,y=EOF_3d(data_3d,lat[2:37],lon[48:99],10)
/ h; F9 b5 w1 l$ O- m
4 Y, p* v: p1 O5 H) u #绘图1 X" E0 v8 b2 \9 O
#BlueWhiteOrangeRed/ `. v4 ]/ T3 {- |8 x% E
plot=[]) Q% ?- _. l% F% q+ a5 w
wks_type = "png"#子系统没有可视化模块,输出成png图片在打开来看
1 ?' |% C2 P5 r* H7 `# L' _! j wks = Ngl.open_wks(wks_type,"/mnt/c/Users/59799/Desktop/EOF_first_model")#图片保存到本地的桌面6 N; K: G' ]7 v2 ?) b: f
cnres = Ngl.Resources()3 @. Z! `7 Q0 S/ c+ \8 `
cnres = Ngl.Resources()
: e+ C( K* k- H K" ?7 O cnres.nglDraw = False#因为要绘制叠加图先阻止画图8 [5 @3 Q K k& k1 o8 f# K" C# h
cnres.nglFrame = False#关闭框架与上一条搭配使用! ?" Y) ]! b+ {0 s1 O4 t; u1 N2 Z
cmap = Ngl.read_colormap_file("BlueWhiteOrangeRed")#读取colorbar,官网可查啊
" y- G& _$ c2 I% u! Q2 W) K cnres.cnFillPalette = cmap[:,:]#不完整使用整个色图,只是用颜色定义值的第五行开始2 N( N# D: Z- N3 K+ t
cnres.cnFillOn = True #等值线填充! n' y( H# V$ ]
cnres.cnLinesOn = False#不画等值线! p3 y, [- C2 A, w" d& r( F& }
cnres.cnLineLabelsOn = False#不画等值线标签(等值线数值)
( t+ b9 D$ y4 m/ g/ p' k cnres.lbOrientation = "horizontal"#水平放置colorbar' q( i& q! n, u9 ]7 @
cnres.mpProjection = "Orthographic"
/ u; H2 ]( R6 d5 R5 x% L cnres.sfXArray = lon[48:99]#经度对应为x轴
7 h$ Z M: `1 z [0 T2 | cnres.sfYArray = lat[2:37]#纬度对应为y轴1 N% I2 N( v c5 R
cnres.mpCenterLonF = lon[48:99].mean()+ p1 B$ {! ` l% ~0 \
cnres.mpCenterLatF = lat[2:37].mean()
! F4 Z. J" q/ n9 l9 q, c #cnres.mpLimitMode = "LatLon"#规定经纬度范围时需要使用,否则规定无效# W0 R: k a( k1 P' V8 T
#cnres.mpMinLonF = lon[48:99].min()#不加范围限制则图片为所规定中心纬度经度显示的全球视角(数据只在规定范围有的话会出现无数据的空白区域(海岸线依旧存在)); f# V% h3 X" a' L8 r7 i
#cnres.mpMaxLonF = lon[48:99].max()#不加范围限制则图片为所规定中心纬度经度显示的全球视角(数据只在规定范围有的话会出现无数据的空白区域)1 W5 q( `4 a8 m$ \, x! m9 H
#cnres.mpMinLatF = lat[2:37].min()#不加范围限制则图片为所规定中心纬度经度显示的全球视角(数据只在规定范围有的话会出现无数据的空白区域)6 l+ C3 R8 \. X7 O
#cnres.mpMaxLatF = lat[2:37].max()#不加范围限制则图片为所规定中心纬度经度显示的全球视角(数据只在规定范围有的话会出现无数据的空白区域)
* x) d- G8 ]3 ~5 G- J% Y' R cnres.nglMaximize = False#必须关闭最大化绘图不然下面的调整plot box的参数是无效的
7 q2 S$ G2 N% { |& D9 [ cnres.vpXF = 0.08 # The left side of the box
4 F9 m3 I2 `7 @4 S cnres.vpYF = 0.75 # The top side of the plot box% ~3 S2 i" r" k
cnres.vpWidthF = 0.4 # The Width of the plot box
0 i5 L/ p/ e% L8 C3 H$ F9 c# w cnres.vpHeightF = 0.5 # The height of the plot box/ n! c! @$ N' ]; |3 j9 [
cnres.mpGeophysicalLineThicknessF =3.3 #海岸线宽度(粗细程度) 默认的太小了% Y. k. I$ @& Q# H6 I: E+ W
cnres.mpFillOn = False#不开启地图填充,关于陆地海洋内陆水的填充自然不需要
) z3 c' W8 g' X$ F8 s7 w cnres.tiMainString = "e_first_model"
- \/ N- t/ |2 [' v7 I p1= Ngl.contour_map(wks,e_2d[:,:,0],cnres)
/ Y/ M' j# Z D Z2 U/ N& K* L; T; w8 y" [2 U( W
xyres = Ngl.Resources()
% g8 O% g$ e& ?) e9 g xyres.nglDraw = False#因为要绘制叠加图先阻止画图
1 i$ F/ [" ?1 [( `2 U xyres.nglFrame = False#关闭框架与上一条搭配使用0 \# X7 E, |* y+ l; c
xyres.nglMaximize = False#必须关闭最大化绘图不然下面的调整plot box的参数是无效的
# P: [. u% P' }- [, @: B! e" M5 d- f0 a xyres.vpXF = 0.59 # The left side of the box
- ~4 L" Z& h8 b xyres.vpYF = 0.645 # The top side of the plot box+ }! c9 s: Y3 c$ l
xyres.vpWidthF = 0.4 # The Width of the plot box6 X$ V% @3 j5 Y4 w; z4 [( B8 c3 i
xyres.vpHeightF = 0.32 # The height of the plot box
# t5 X+ f, y: p# j* ~ xyres.tiXAxisString = "Jan of Each Year"
" {+ j6 X. s+ [# `3 w0 s7 T xyres.tmXBMode = "Explicit" # Define your own tick mark labels.
! X4 O0 e) b: h xyres.tmXBValues = list(range(1,20,1))# Values from 1 to 19.
3 [) r L' R, ?" h; o xyres.tmXBLabels = ["00","01","02","03","04","05","06","07","08","09","10","11","12","13","14","15","16","17","18"]6 K3 D8 K( I& V4 y% G1 q
xyres.tiMainString = "p_first_model" k- v. O) n/ Q8 T9 }
x=np.arange(1,21,1)
Y# V5 c$ i3 p; p p2 = Ngl.xy(wks,x,p_t[0],xyres)
2 n( h" O- y& s4 G. r: s5 R8 V
5 |/ H O& s& d; y$ \! B7 r# L) C" u: ^) R
Ngl.draw(p1)
5 ^& O, w* ?: O6 \) w Ngl.draw(p2), g, \+ J# q. d3 x2 P9 v
Ngl.frame(wks)' x+ J# o, M9 Q
Ngl.end() ! ^2 E& o- Z& O+ [3 r
% ~2 L2 V% p0 p+ c5 Y/ _) {
: m* U& X4 f# F% P$ N) b ^; E% X7 _
/ q4 r( t) z: S
6 Y% E+ j4 h6 G# A& @3 u* T3 C' g |