NetLogo编程指南(三)

翻译整理自: NetLogo Home Page

本节详细介绍 NetLogo 编程语言。

通篇提到的代码示例模型可以在模型库的代码示例部分找到。

目录

(一)

(二)

(三)

(四)

(五)

(六)

列表

在最简单的模型中,每个变量只包含一条信息,通常是数字或字符串。列表使您可以通过将信息收集到列表中来将多条信息存储在一个值中。列表中的每个值都可以是任何类型的值:数字或字符串、代理或代理集,甚至是另一个列表。

列表允许在 NetLogo 中方便地打包信息。如果您的代理对多个变量进行重复计算,使用列表变量可能比使用多个数字变量更容易。几个原语简化了对列表中的每个值执行相同计算的过程。

NetLogo 字典 有一个部分列出了所有与列表相关的原语。

常量列表

您可以通过简单地将您想要的值放在括号之间的列表中来创建列表,如下所示:set mylist [2 4 6 8]。请注意,各个值由空格分隔。您可以通过这种方式创建包含数字和字符串的列表,以及列表中的列表,例如[[2 4] [3 5]]。

空列表是通过在括号之间不放置任何内容来编写的,如下所示:[]。

即时构建列表

如果您想制作一个列表,其中的值由报告者确定,而不是一系列常量,请使用 list 报告者。记者接受另外 list 两个记者,运行它们,并将结果报告为列表。

如果我想要一个包含两个随机值的列表,我可能会使用以下代码:

set random-list list (random 10) (random 20)

这将random-list在每次运行时设置为两个随机整数的新列表。

要制作更长或更短的列表,您可以使用 list 少于或多于两个输入的记者,但为了这样做,您必须将整个调用括在括号中,例如:

(list random 10)
(list random 10 random 20 random 30)

有关详细信息,请参阅 不同数量的输入

使用报告器最容易构建某些类型的列表 n-values ,它允许您通过重复运行给定的报告器来构建特定长度的列表。你可以制作一个重复的相同值的列表,或者一个范围内的所有数字,或者很多随机数,或者许多其他的可能性。有关详细信息和示例,请参阅词典条目。

of 原语允许您从代理集构建列表。它报告一个列表,其中包含给定报告者的每个代理的值。(报告者可以是一个简单的变量名,也可以是一个更复杂的表达式——甚至是对使用 . 定义的过程的调用 to-report 。)一个常见的习惯用法是

max [...] of turtles
sum [...] of turtles

等等。

您可以使用报告器组合两个或多个列表, sentence 报告器通过将列表的内容组合成一个更大的列表来连接列表。像 list , sentence 通常需要两个输入,但如果调用被括号括起来,则可以接受任意数量的输入。

更改列表项

从技术上讲,列表无法修改,但您可以根据旧列表构建新列表。如果您希望新列表替换旧列表,请使用 set . 例如:

set mylist [2 7 5 Bob [3 0 -2]]
; mylist is now [2 7 5 Bob [3 0 -2]]
set mylist replace-item 2 mylist 10
; mylist is now [2 7 10 Bob [3 0 -2]]

replace-item 记者接受了三个输入。第一个输入指定要更改列表中的哪个项目。0 表示第一项,1 表示第二项,依此类推。

要将项目(例如 42)添加到列表的末尾,请使用 lput 报告器。( fput 将一个项目添加到列表的开头。)

set mylist lput 42 mylist
; mylist is now [2 7 10 Bob [3 0 -2] 42]

但如果你改变主意呢?( but-last 简称bl)报告者报告除最后一项之外的所有列表项。

set mylist but-last mylist
; mylist is now [2 7 10 Bob [3 0 -2]]

假设您要删除项目 0,即列表开头的 2。

set mylist but-first mylist
; mylist is now [7 10 Bob [3 0 -2]]

假设您想将嵌套在项目 3 中的第三个项目从 -2 更改为 9?关键是要意识到可以用来调用嵌套列表 [3 0 -2] 的名称是item 3 mylist. 然后 replace-item 可以嵌套报告器以更改列表中的列表。为清楚起见添加了括号。

set mylist (replace-item 3 mylist
                  (replace-item 2 (item 3 mylist) 9))
; mylist is now [7 10 Bob [3 0 9]]

遍历列表

如果你想依次对列表中的每一项进行一些操作, foreach 命令和 map 报告器可能会有所帮助。

foreach 用于对列表中的每个项目运行一个或多个命令。它需要一个输入列表和一个命令名称或命令块,如下所示:

foreach [1 2 3] show
foreach [2 4 6]
  [ n -> crt n
    show (word "created " n " turtles") ]
=> created 2 turtles
=> created 4 turtles
=> created 6 turtles

在块中,变量n保存输入列表中的当前值。

以下是更多示例 foreach

foreach [1 2 3] [ steps -> ask turtles [ fd steps ] ]
;; turtles move forward 6 patches
foreach [true false true true] [ should-move? -> ask turtles [ if should-move? [ fd 1 ] ] ]
;; turtles move forward 3 patches

map 类似 foreach ,不过是记者。它需要一个输入列表和一个报告者姓名或报告者块。请注意,与 不同 foreach 的是,记者首先出现,如下所示:

show map round [1.2 2.2 2.7]
;; prints [1 2 3]

map 报告一个列表,其中包含将报告器应用于输入列表中每个项目的结果。同样,使用匿名过程中命名的变量(x在下面的示例中)来引用列表中的当前项。

以下是更多示例 map

show map [ x -> x < 0 ] [1 -1 3 4 -2 -10]
;; prints [false true false false true true]
show map [ x -> x * x ] [1 2 3]
;; prints [1 4 9]

除了mapand之外foreach,其他用于以可配置方式处理整个列表的原语包括 filter reduce sort-by

这些原语并不总是适用于您想要对整个列表进行操作的每种情况的解决方案。在某些情况下,您可能需要使用一些其他技术,例如使用 repeat or的循环 while 或递归过程。

我们提供给这些示例的代码块map实际上foreach是 匿名过程 匿名过程在下面的匿名 过程中有更详细的解释。

不同数量的输入

一些涉及列表和字符串的命令和报告器可能需要不同数量的输入。在这些情况下,为了向它们传递一些默认值以外的输入,原语及其输入必须用括号括起来。这里有些例子:

show list 1 2
=> [1 2]
show (list 1 2 3 4)
=> [1 2 3 4]
show (list)
=> []

请注意,这些特殊原语中的每一个都有默认数量的输入,不需要括号。具有此功能的基元是 list , word , sentence , map , foreach , run , 和 runresult

代理商名单

早些时候,我们说过代理集总是以随机顺序排列,每次都是不同的随机顺序。如果你需要你的代理人以固定的顺序做某事,你需要制作一个代理人列表。

有两个原语可以帮助您做到这一点, sort sort-by .

sort 和都 sort-by 可以将代理集作为输入。结果始终是一个新列表,其中包含与代理集相同的代理,但以特定顺序排列。

如果你 sort 在海龟代理集上使用,结果是一个按数字升序排序的海龟列表 who

如果您 sort 在一个补丁代理集上使用,结果是一个从左到右、从上到下排序的补丁列表。

如果您 sort 在一个链接代理集上使用,结果是一个链接列表,首先按升序排序, end1 然后 end2 任何剩余的关系按照它们在“代码”选项卡中声明的顺序按品种解析。

如果您需要降序,您可以结合 reverse 使用 sort ,例如reverse sort turtles。

如果您希望您的代理按照标准代理使用的标准之外的其他标准进行排序,则 sort 需要改为使用 sort-by

这是一个例子:

sort-by [ [a b] -> [size] of a < [size] of b ] turtles

这将返回一个按海龟变量升序排序的海龟列表 size

of 有一种常见的模式可以使用和的组合以随机顺序获取代理列表 self ,在极少数情况下您不能只使用 ask

[self] of my-agentset

询问代理商名单

有了代理人列表后,您可能想要求他们每个人做某事。为此,请结合使用 foreach ask 命令,如下所示:

foreach sort turtles [ the-turtle ->
  ask the-turtle [
]

这将按谁的数字升序询问每只乌龟。将“海龟”替换为“补丁”,以从左到右、从上到下的顺序询问补丁。

请注意,您不能 ask 直接在海龟列表上使用。 ask 仅适用于代理集和单个代理。

列表的性能

NetLogo 列表的数据结构是一个复杂的基于树的数据结构,大多数操作在几乎恒定的时间内运行。这包括fput, lput, butfirst, butlast, length,item和replace-item。

快速性能规则的一个例外是连接两个列表sentence需要遍历和复制整个第二个列表。(这可能会在未来的版本中修复。)

从技术上讲,“近恒定时间”实际上是对数时间,与底层树的深度成正比,但这些树具有大节点和高分支因子,因此它们的深度永远不会超过几层。这意味着最多可以在几个步骤中进行更改。这些树是不可变的,但它们彼此共享结构,因此不需要复制整棵树来制作更改版本。

实际使用的数据结构是来自 Scala 集合库的不可变 Vector 类。这些是 32-wide hash array mapped tries ,由 Tiark Rompf 实现,部分基于 Phil Bagwell 和 Rich Hickey 的工作。

数学

NetLogo 中的所有数字都在内部存储为双精度浮点数,如 IEEE 754 标准中所定义。它们是 64 位数字,由一个符号位、一个 11 位指数和一个 52 位尾数组成。有关详细信息,请参阅 IEEE 754 标准。

NetLogo 中的“整数”只是一个恰好没有小数部分的数字。3和3.0之间没有区别;他们是同一个号码。(这与大多数人在日常环境中使用数字的方式相同,但与某些编程语言不同。某些语言将整数和浮点数视为不同的类型。)

整数总是由 NetLogo 打印而没有尾随的“.0”:

show 1.5 + 1.5
observer: 3

如果在需要整数的上下文中提供了带有小数部分的数字,则小数部分将被简单地丢弃。例如,crt 3.5创建三只乌龟;多余的 0.5 将被忽略。

整数范围是 +/-9007199254740992(2^53,大约 9 千万亿)。超出此范围的计算不会导致运行时错误,但当舍入最低有效(二进制)数字以使数字适合 64 位时,精度将会丢失。对于非常大的数字,这种舍入可能会导致不精确的答案,这可能会令人惊讶:

show 2 ^ 60 + 1 = 2 ^ 60
=> true

如果涉及分数数量,较小数字的计算也会产生令人惊讶的结果,因为并非所有分数都可以精确表示并且可能会发生舍入。例如:

show 1 / 6 + 1 / 6 + 1 / 6 + 1 / 6 + 1 / 6 + 1 / 6
=> 0.9999999999999999
show 1 / 9 + 1 / 9 + 1 / 9 + 1 / 9 + 1 / 9 + 1 / 9 + 1 / 9 + 1 / 9 + 1 / 9
=> 1.0000000000000002

任何产生特殊量“无穷大”或“不是数字”的操作都会导致运行时错误。

科学计数法

NetLogo 使用“科学计数法”显示非常大或非常小的浮点数。例子:

show 0.000000000001
=> 1.0E-12
show 50000000000000000000
=> 5.0E19

科学记数法中的数字以字母 E(表示“指数”)的存在来区分。它的意思是“乘以 10 的次方”,例如,1.0E-12 表示 1.0 乘以 10 的 -12 次方:

show 1.0 * 10 ^ -12
=> 1.0E-12

您也可以自己在 NetLogo 代码中使用科学记数法:

show 3.0E6
=> 3000000
show 8.123456789E6
=> 8123456.789
show 8.123456789E7
=> 8.123456789E7
show 3.0E16
=> 3.0E16
show 8.0E-3
=> 0.0080
show 8.0E-4
=> 8.0E-4

这些示例表明,如果指数小于 -3 或大于 6,带小数部分的数字将使用科学记数法显示。NetLogo 的整数范围 -9007199254740992 到 9007199254740992 (+/-2^53) 之外的数字也始终显示在科学计数法:

show 2 ^ 60
=> 1.15292150460684698E18

输入数字时,字母 E 可以是大写或小写。打印数字时,NetLogo 总是使用大写的 E:

show 4.5e20
=> 4.5E20

浮点精度

由于 NetLogo 中的数字受浮点数如何用二进制表示的限制,您可能会得到稍微不准确的答案。例如:

show 0.1 + 0.1 + 0.1
=> 0.30000000000000004
show cos 90
=> 6.123233995736766E-17

这是浮点运算的固有问题。它出现在所有使用浮点数的编程语言中。

如果您要处理固定精度的数量,例如美元和美分,一种常见的技术是在内部仅使用整数(美分),然后除以 100 以获得以美元为单位的结果以供显示。

如果您必须使用浮点数,那么在某些情况下您可能需要替换一个简单的相等性测试,例如if x = 1 [ ... ]用一个可以容忍轻微不精确的测试,例如if abs (x - 1) < 0.0001 [ ... ]。

此外,该 precision 原语对于出于显示目的四舍五入数字很方便。NetLogo 监视器也将它们显示的数字四舍五入到可配置的小数位。

随机数

NetLogo 使用的随机数是所谓的“伪随机数”。(这在计算机编程中很典型。)这意味着它们看起来是随机的,但实际上是由确定性过程生成的。“确定性”意味着如果你从相同的随机“种子”开始,你每次都会得到相同的结果。我们稍后会解释“种子”的含义。

在科学建模的背景下,伪随机数实际上是可取的。这是因为科学实验的可重复性很重要——这样任何人都可以自己尝试并获得与您相同的结果。由于 NetLogo 使用伪随机数,您用它做的“实验”可以被其他人复制。

这是它的工作原理。NetLogo 的随机数生成器可以从特定的种子值开始,该种子值必须是 -2147483648 到 2147483647 范围内的整数。一旦生成器被 random-seed 命令“播种”,从那时起它总是生成相同的随机数序列. 例如,如果您运行这些命令:

random-seed 137
show random 100
show random 100
show random 100

您将始终按此顺序获得数字 79、89 和 61。

但是请注意,如果您使用相同版本的 NetLogo,则只能保证获得相同的数字。有时当我们制作新版本的 NetLogo 时,随机数生成器会发生变化。(目前,我们使用称为 Mersenne Twister 的生成器。)

要创建适合播种随机数生成器的数字,请使用 new-seed 记者。 new-seed 根据当前日期和时间创建一个种子,均匀分布在可能的种子空间中。它从不连续两次报告相同的种子。

代码示例: 随机种子示例

如果您不自己设置随机种子,NetLogo 会将其设置为基于当前日期和时间的值。没有办法知道它选择了什么随机种子,所以如果你想让你的模型运行可重现,你必须提前自己设置随机种子。

名称中带有“随机”的 NetLogo 原语(random、random-float 等)并不是唯一使用伪随机数的原语。许多其他操作也进行随机选择。例如,agentsets 始终是随机顺序,随机 one-of 选择 n-of agents, sprout 命令创建具有随机颜色和标题的海龟,并且 downhill 报告者在出现平局时选择随机补丁。所有这些随机选择也由随机种子控制,因此模型运行可以重现。

random 除了 和 生成的均匀分布的随机整数和浮点数 random-float ,NetLogo 还提供其他几种随机分布。请参阅 random-normal random-poisson random-exponential 和的字典条目 random-gamma

辅助发电机

由按钮或命令中心运行的代码使用主随机数生成器。

监视器中的代码使用辅助随机生成器,因此即使监视器使用随机数进行计算,模型的结果也不会受到影响。滑块中的代码也是如此。

局部随机性

您可能想要明确指定一段代码不影响主随机生成器的状态,因此模型的结果不受影响。该 with-local-randomness 命令就是为此目的而提供的。有关详细信息,请参阅 NetLogo 词典中的条目。

为运行保存随机种子

如果你想知道模型“运行”使用的随机种子是什么,你可以添加一些简单的代码来跟踪它。然后,如果出现有趣的行为或出现间歇性错误情况,您可以通过获取种子并重新使用它来重现运行。

添加一个starting-seed全局变量,然后在您的过程中 new-seed 使用后使用为其赋值。然后将该值提供给命令,以便它将用于运行的其余部分。 clear-all setup random-seed

globals [starting-seed]
to setup
  clear-all
  set starting-seed new-seed
  random-seed starting-seed