【问题标题】:Decay chain simulation - with significantly different time scales衰减链模拟 - 具有显着不同的时间尺度
【发布时间】:2022-09-23 06:14:29
【问题描述】:

我想用 Python 模拟一个衰减链。通常,(在所有核素的循环中)计算每个时间步的衰变数并更新母核和子核的数量。

我的问题是衰变链包含在非常不同的时间尺度上的半衰期,即 Po-214 为 0.0001643 秒,Ra-226 为 307106512477175.9 秒(= 1600 年)。

对所有核素使用相同的时间步似乎没有用。

有没有一种模拟方法,最好是在 Python 中,可以用来处理这种情况?

    标签: simulation physics montecarlo


    【解决方案1】:

    不要为此使用时间步长。使用事件调度。

    半衰期可以表示为指数衰减,conversion between half life and rate of decay is straightforward。从两种类型的原子核的数量开始,并安排指数级的事件间时间,以确定每种类型的下一次衰变何时发生。无论哪种类型的时间较短,减少相应的原子核数量并为该类型安排下一次衰变(如果需要,增加它衰变到的任何物质的计数)。

    这可以很容易地推广到多种不同的事件类型,方法是使用按发生时间排序的优先级队列来确定哪个事件将是下一个执行的事件。这是离散事件模拟背后的基本原理。

    更新

    这种方法适用于单个衰减事件,但是当我们有指数事件间时间时,我们可以利用两个重要属性。

    1. 首先要注意指数分布的事件间时间意味着这些是泊松过程。 superposition property tells us that the union of two independent Poisson processes, each having rate λ, is a Poisson process with rate 2λ。简单的归纳表明,如果我们有n具有相同速率的独立泊松性质,它们的叠加是具有速率的泊松过程nλ。

    2. 第二个特性是指数分布是无记忆的。这意味着当泊松事件发生时,我们可以通过以当前速率生成新的指数分布时间并将其添加到当前时间来生成下一个事件的时间。

      你没有提供任何关于你想要的输出方式的信息,所以我武断地决定打印一份报告,显示时间和核素的当前数量,只要该数量减半。考虑到 Po-214 的长半衰期,我还每 10 年打印一次报告。

      我使用帖子顶部提供的链接将半衰期转换为费率,然后转为意味着,因为这就是 Python numpy 的指数生成器被参数化使用。这是一个简单的转换,因为均值和利率是彼此的倒数。

      这是一个带有 cmets 的 Python 实现:

      from numpy.random import default_rng
      from math import log
      
      rng = default_rng()
      
      # This creates a list of entries of quantities that will trigger a report.
      # I've chosen to go with successive halvings of the original quantity.
      def generate_report_qtys(n0):
          report_qty = []
          divisor = 2
          while divisor < n0:
              report_qty.append(n0 // divisor)        # append next half-life qty to array
              divisor *= 2
          return report_qty
      
      seconds_per_year = 365.25 * 24 * 60 * 60
      po_214_half_life = 0.0001643 # seconds
      ra_226_half_life = 1590 * seconds_per_year
      
      log_2 = log(2)
      po_mean = po_214_half_life / log_2  # per-nuclide decay rate for po_214
      ra_mean = ra_226_half_life / log_2  # ditto for ra_226
      
      po_n = po_n0 = 1_000_000_000
      ra_n = ra_n0 = 1_000_000_000
      
      time = 0.0
      
      # Generate a report when the following sets of half-lifes are reached
      po_report_qtys = generate_report_qtys(po_n0)
      ra_report_qtys = generate_report_qtys(ra_n0)
      
      # Initialize first event times for each type of event:
      #  - first entry is polonium next event time
      #  - second entry is radium next event time
      #  - third entry is next ten year report time
      next_event_time = [
          rng.exponential(po_mean / po_n),
          rng.exponential(ra_mean / ra_n),
          10 * seconds_per_year
      ]
      
      # Print column labels and initial values
      print("time,po_214,ra_226,time_in_years")
      print(f"{time},{po_n},{ra_n},{time / seconds_per_year}")
      
      while time < ra_226_half_life:
          # Find the index of the next event time. Index tells us the event type.
          min_index = next_event_time.index(min(next_event_time))
          if min_index == 0:
              po_n -= 1   # decrement polonium count
              time = next_event_time[0]   # update clock to the event time
              if po_n > 0:
                  next_event_time[0] += rng.exponential(po_mean / po_n)   # determine next event time for po
              else:
                  next_event_time[0] = float('Inf')
              # print report if this is a half-life occurrence
              if len(po_report_qtys) > 0 and po_n == po_report_qtys[0]:
                  po_report_qtys.pop(0)   # remove this occurrence from the list
                  print(f"{time},{po_n},{ra_n},{time / seconds_per_year}")
          elif min_index == 1:
              # same as above, but for radium
              ra_n -= 1
              time = next_event_time[1]
              if ra_n > 0:
                  next_event_time[1] += rng.exponential(ra_mean / ra_n)
              else:
                  next_event_time[1] = float('Inf')
              if len(ra_report_qtys) > 0 and ra_n == ra_report_qtys[0]:
                  ra_report_qtys.pop(0)
                  print(f"{time},{po_n},{ra_n},{time / seconds_per_year}")
          else:
              # update clock, print ten year report
              time = next_event_time[2]
              next_event_time[2] += 10 * seconds_per_year
              print(f"{time},{po_n},{ra_n},{time / seconds_per_year}")
      

      运行时间与核素数量成正比。在我的 M1 MacBook Pro 上运行 10 亿个每个需要 831.28 秒,而每个 100 万个需要 2.19 秒。我还将它移植到 Crystal,这是一种编译后的类 Ruby 语言,它在 32 秒内为每个核素产生了 10 亿个类似的结果。如果您打算运行较大规模的问题,我建议您使用编译语言,但我还要指出,如果您像我一样使用半衰期报告,那么对于较小的人口规模,结果几乎相同,但获得的速度要快得多。

      我还建议,如果您想将这种方法用于更复杂的模型,您应该使用包含时间和事件类型的元组优先级队列来存储待处理的未来事件集,而不是简单的列表。

      最后但同样重要的是,这里有一些示例输出:

      time,po_214,ra_226,time_in_years
      0.0,1000000000,1000000000,0.0
      0.0001642985647308265,500000000,1000000000,5.20630734690935e-12
      0.0003286071415481526,250000000,1000000000,1.0412931957694901e-11
      0.0004929007624958987,125000000,1000000000,1.5619082645571865e-11
      0.0006571750701843468,62500000,1000000000,2.082462133319222e-11
      0.0008214861652253772,31250000,1000000000,2.6031325741671646e-11
      0.0009858208114474198,15625000,1000000000,3.1238776442043114e-11
      0.0011502417677631668,7812500,1000000000,3.6448962144243124e-11
      0.0013145712145548718,3906250,1000000000,4.165624808460947e-11
      0.0014788866075394896,1953125,1000000000,4.686308868670272e-11
      0.0016432124609700412,976562,1000000000,5.2070260760325286e-11
      0.001807832817519779,488281,1000000000,5.728676507465013e-11
      0.001972981254301889,244140,1000000000,6.252000324175124e-11
      0.0021372947080755688,122070,1000000000,6.772678239395799e-11
      0.002301139510796509,61035,1000000000,7.29187108904514e-11
      0.0024642826956509244,30517,1000000000,7.808840645837847e-11
      0.0026302282280720344,15258,1000000000,8.33469030620844e-11
      0.0027944471221414947,7629,1000000000,8.855068579808016e-11
      0.002954014120737834,3814,1000000000,9.3607058861822e-11
      0.0031188370035748177,1907,1000000000,9.882998084692174e-11
      0.003282466175503322,953,1000000000,1.0401507641592902e-10
      0.003457552492113242,476,1000000000,1.0956322699169905e-10
      0.003601851131916978,238,1000000000,1.1413577496124477e-10
      0.0037747824699194033,119,1000000000,1.1961563838566314e-10
      0.0039512825256332275,59,1000000000,1.252085876503038e-10
      0.004124330529803301,29,1000000000,1.3069214800248755e-10
      0.004337121375518753,14,1000000000,1.3743508300754027e-10
      0.004535068261934763,7,1000000000,1.437076413268044e-10
      0.004890820999020369,3,1000000000,1.5498076529965425e-10
      0.004909065046898487,1,1000000000,1.555588842908994e-10
      315576000.0,0,995654793,10.0
      631152000.0,0,991322602,20.0
      946728000.0,0,987010839,30.0
      1262304000.0,0,982711723,40.0
      1577880000.0,0,978442651,50.0
      1893456000.0,0,974185269,60.0
      2209032000.0,0,969948418,70.0
      2524608000.0,0,965726762,80.0
      2840184000.0,0,961524848,90.0
      3155760000.0,0,957342148,100.0
      3471336000.0,0,953178898,110.0
      3786912000.0,0,949029294,120.0
      4102488000.0,0,944898063,130.0
      4418064000.0,0,940790494,140.0
      4733640000.0,0,936699123,150.0
      5049216000.0,0,932622334,160.0
      5364792000.0,0,928565676,170.0
      5680368000.0,0,924523267,180.0
      5995944000.0,0,920499586,190.0
      6311520000.0,0,916497996,200.0
      6627096000.0,0,912511030,210.0
      6942672000.0,0,908543175,220.0
      7258248000.0,0,904590364,230.0
      7573824000.0,0,900656301,240.0
      7889400000.0,0,896738632,250.0
      8204976000.0,0,892838664,260.0
      8520552000.0,0,888956681,270.0
      8836128000.0,0,885084855,280.0
      9151704000.0,0,881232862,290.0
      9467280000.0,0,877401861,300.0
      9782856000.0,0,873581425,310.0
      10098432000.0,0,869785364,320.0
      10414008000.0,0,866002042,330.0
      10729584000.0,0,862234212,340.0
      11045160000.0,0,858485627,350.0
      11360736000.0,0,854749939,360.0
      11676312000.0,0,851032010,370.0
      11991888000.0,0,847329028,380.0
      12307464000.0,0,843640016,390.0
      12623040000.0,0,839968529,400.0
      12938616000.0,0,836314000,410.0
      13254192000.0,0,832673999,420.0
      13569768000.0,0,829054753,430.0
      13885344000.0,0,825450233,440.0
      14200920000.0,0,821859757,450.0
      14516496000.0,0,818284787,460.0
      14832072000.0,0,814727148,470.0
      15147648000.0,0,811184419,480.0
      15463224000.0,0,807655470,490.0
      15778800000.0,0,804139970,500.0
      16094376000.0,0,800643280,510.0
      16409952000.0,0,797159389,520.0
      16725528000.0,0,793692735,530.0
      17041104000.0,0,790239221,540.0
      17356680000.0,0,786802135,550.0
      17672256000.0,0,783380326,560.0
      17987832000.0,0,779970864,570.0
      18303408000.0,0,776576174,580.0
      18618984000.0,0,773197955,590.0
      18934560000.0,0,769836170,600.0
      19250136000.0,0,766488931,610.0
      19565712000.0,0,763154778,620.0
      19881288000.0,0,759831742,630.0
      20196864000.0,0,756528400,640.0
      20512440000.0,0,753237814,650.0
      20828016000.0,0,749961747,660.0
      21143592000.0,0,746699940,670.0
      21459168000.0,0,743450395,680.0
      21774744000.0,0,740219531,690.0
      22090320000.0,0,736999181,700.0
      22405896000.0,0,733793266,710.0
      22721472000.0,0,730602000,720.0
      23037048000.0,0,727427544,730.0
      23352624000.0,0,724260327,740.0
      23668200000.0,0,721110260,750.0
      23983776000.0,0,717973915,760.0
      24299352000.0,0,714851218,770.0
      24614928000.0,0,711740161,780.0
      24930504000.0,0,708645945,790.0
      25246080000.0,0,705559170,800.0
      25561656000.0,0,702490991,810.0
      25877232000.0,0,699436919,820.0
      26192808000.0,0,696394898,830.0
      26508384000.0,0,693364883,840.0
      26823960000.0,0,690348242,850.0
      27139536000.0,0,687345934,860.0
      27455112000.0,0,684354989,870.0
      27770688000.0,0,681379178,880.0
      28086264000.0,0,678414567,890.0
      28401840000.0,0,675461363,900.0
      28717416000.0,0,672522494,910.0
      29032992000.0,0,669598412,920.0
      29348568000.0,0,666687807,930.0
      29664144000.0,0,663787671,940.0
      29979720000.0,0,660901676,950.0
      30295296000.0,0,658027332,960.0
      30610872000.0,0,655164886,970.0
      30926448000.0,0,652315268,980.0
      31242024000.0,0,649481821,990.0
      31557600000.0,0,646656096,1000.0
      31873176000.0,0,643841377,1010.0
      32188752000.0,0,641041609,1020.0
      32504328000.0,0,638253759,1030.0
      32819904000.0,0,635479981,1040.0
      33135480000.0,0,632713706,1050.0
      33451056000.0,0,629962868,1060.0
      33766632000.0,0,627223350,1070.0
      34082208000.0,0,624494821,1080.0
      34397784000.0,0,621778045,1090.0
      34713360000.0,0,619076414,1100.0
      35028936000.0,0,616384399,1110.0
      35344512000.0,0,613702920,1120.0
      35660088000.0,0,611035112,1130.0
      35975664000.0,0,608376650,1140.0
      36291240000.0,0,605729994,1150.0
      36606816000.0,0,603093946,1160.0
      36922392000.0,0,600469403,1170.0
      37237968000.0,0,597854872,1180.0
      37553544000.0,0,595254881,1190.0
      37869120000.0,0,592663681,1200.0
      38184696000.0,0,590085028,1210.0
      38500272000.0,0,587517782,1220.0
      38815848000.0,0,584961743,1230.0
      39131424000.0,0,582420312,1240.0
      39447000000.0,0,579886455,1250.0
      39762576000.0,0,577362514,1260.0
      40078152000.0,0,574849251,1270.0
      40393728000.0,0,572346625,1280.0
      40709304000.0,0,569856166,1290.0
      41024880000.0,0,567377753,1300.0
      41340456000.0,0,564908008,1310.0
      41656032000.0,0,562450828,1320.0
      41971608000.0,0,560005832,1330.0
      42287184000.0,0,557570018,1340.0
      42602760000.0,0,555143734,1350.0
      42918336000.0,0,552729893,1360.0
      43233912000.0,0,550326162,1370.0
      43549488000.0,0,547932312,1380.0
      43865064000.0,0,545550017,1390.0
      44180640000.0,0,543178924,1400.0
      44496216000.0,0,540814950,1410.0
      44811792000.0,0,538462704,1420.0
      45127368000.0,0,536123339,1430.0
      45442944000.0,0,533792776,1440.0
      45758520000.0,0,531469163,1450.0
      46074096000.0,0,529157093,1460.0
      46389672000.0,0,526854383,1470.0
      46705248000.0,0,524564196,1480.0
      47020824000.0,0,522282564,1490.0
      47336400000.0,0,520011985,1500.0
      47651976000.0,0,517751635,1510.0
      47967552000.0,0,515499791,1520.0
      48283128000.0,0,513257373,1530.0
      48598704000.0,0,511022885,1540.0
      48914280000.0,0,508798440,1550.0
      49229856000.0,0,506582663,1560.0
      49545432000.0,0,504379227,1570.0
      49861008000.0,0,502186693,1580.0
      50176584000.0,0,500000869,1590.0
      

      扩展超过 2 个核素

      我提到过,对于多个核素,您需要使用优先级队列来跟踪接下来会发生哪些衰变。我围绕函数重新组织了代码,但这样可以更灵活地扩展问题的范围。干得好:

      #!/usr/bin/env python3
      
      from numpy.random import default_rng
      from math import log
      import heapq
      
      SECONDS_PER_YEAR = 365.25 * 24 * 60 * 60
      LOG_2 = log(2)
      
      rng = default_rng()
      
      def generate_report_qtys(n0):
          report_qty = []
          divisor = 2
          while divisor < n0:
              report_qty.append(n0 // divisor)        # append next half-life qty to array
              divisor *= 2
          return report_qty
      
      po_n0 = 10_000_000
      ra_n0 = 10_000_000
      mu_n0 = 10_000_000
      
      # mean is half-life / LOG_2
      properties = dict(
          po_214 = dict(
              mean = 0.0001643 / LOG_2,
              qty = po_n0,
              report_qtys = generate_report_qtys(po_n0)
          ),
          ra_226 = dict(
              mean = 1590 * SECONDS_PER_YEAR / LOG_2,
              qty = ra_n0,
              report_qtys = generate_report_qtys(ra_n0)
          ),
          made_up = dict(
              mean = 75 * SECONDS_PER_YEAR / LOG_2,
              qty = mu_n0,
              report_qtys = generate_report_qtys(mu_n0)       
          )
      )
      
      nuclide_names = [name for name in properties.keys()]
      
      def population_mean(nuclide):
          return properties[nuclide]['mean'] / properties[nuclide]['qty']
      
      def report():   # isolate as single point of maintenance even though it's a one-liner
          nuc_qtys = [str(properties[nuclide]['qty']) for nuclide in nuclide_names]
          print(f"{time},{time / SECONDS_PER_YEAR}," + ','.join(nuc_qtys))
      
      def decay_event(nuclide):
          properties[nuclide]['qty'] -= 1
          current_qty = properties[nuclide]['qty']
          if current_qty > 0:
              heapq.heappush(event_q, (time + rng.exponential(population_mean(nuclide)), nuclide))
          rep_qty = properties[nuclide]['report_qtys']
          if len(rep_qty) > 0 and current_qty == rep_qty[0]:
              rep_qty.pop(0)  # remove this occurrence from the list
              report()
      
      def report_event():
          heapq.heappush(event_q, (time + 10 * SECONDS_PER_YEAR, 'report_event'))
          report()
      
      event_q = [(rng.exponential(population_mean(nuclide)), nuclide) for nuclide in nuclide_names]
      event_q.append((0.0, "report_event"))
      
      heapq.heapify(event_q)
      
      time = 0.0   # simulated time
      print("time(seconds),time(years)," + ','.join(nuclide_names))   # column labels
      
      while time < 1600 * SECONDS_PER_YEAR:
          time, event_id = heapq.heappop(event_q)
          if event_id == 'report_event':
              report_event()
          else:
              decay_event(event_id)
      

      要添加更多核素,请按照当前条目的模板向properties 字典添加更多条目。

    【讨论】:

    • 太谢谢了。意思是不是单身的事件?这在大时间尺度和 10^n 个事件上如何工作? (n大)?你有任何提示重新实现。在蟒蛇?
    • @TomS 抱歉延迟回复,现实生活干预了。让我知道示例代码和 cmets 是否让您满意。
    • 多谢;我将仔细研究一下,以将其应用于具有十个核素的完整衰变链
    猜你喜欢
    • 2014-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-28
    • 2021-11-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多