HandieAndy

ImmersiveRailroadingTrainControl

Nov 17th, 2018
844
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 13.70 KB | None | 0 0
  1. --[[
  2. Author: Andrew Lalis
  3. File: train_control.lua
  4. Version: 1.0
  5. Last Modified: 19-11-2018
  6.    
  7. This script provides simple ways to set target velocities and distances for
  8. trains from the mod Immersive Engineering. To set it up, place two augments,
  9. one detector and one controller, on the same rail tie, and connect both to a
  10. computer.
  11.  
  12. This script focuses on two major actions:
  13.     1. Slowing down the train with brakes.
  14.     2. Speeding up with the throttle.
  15.  
  16. --]]
  17.  
  18. local DEBUG = false
  19. local VERBOSE = false
  20.  
  21. --[[
  22. This function can be implemented by users for added functionality.
  23.  
  24. stock - table: The result of detector.info()
  25. consist - table: The result of detector.consist()
  26. controller - augment: The locomotive controller augment.
  27. --]]
  28. local function onTrainOverhead(stock, consist, controller)
  29.     controller.horn()
  30. end
  31.  
  32. --[[
  33. How many decimal points of precision should throttle setting have?
  34. Set it to a power of 0.1. Note that more precision means MUCH MORE
  35. computational complexity.
  36. --]]
  37. local THROTTLE_PRECISION = 0.001
  38.  
  39. local CONFIG_FILE = "ir_speed_control_config"
  40.  
  41.  
  42. -- Only enable OC-specific modules if not in debug.
  43. local component = nil
  44. local event = nil
  45. local fs = nil
  46. local serialization = nil
  47. local term = nil
  48.  
  49. local detector = nil
  50. local control = nil
  51.  
  52. if (not DEBUG) then
  53.     component = require("component")
  54.     event = require("event")
  55.     fs = require("filesystem")
  56.     serialization = require("serialization")
  57.     term = require("term")
  58.     detector = component.ir_augment_detector
  59.     control = component.ir_augment_control
  60. end
  61.  
  62. local SPEED_RATIO = 20 * 3.6
  63. local SLIP_CONSTANT = 1.0
  64. local LOCO_PREFIX = "rolling_stock/locomotives/"
  65.  
  66. local function toMetric(v)
  67.     return v * SPEED_RATIO
  68. end
  69.  
  70. local function fromMetric(v)
  71.     return v / SPEED_RATIO
  72. end
  73.  
  74. local function getSlip(v)
  75.     -- TODO: Include weather and biome conditions.
  76.     return 1 - 0.004 * math.abs(v)
  77. end
  78.  
  79. --[[
  80. Extracts the locomotive's name from a json filename as given by detector.info()
  81.  
  82. id - string: The string identifying the type of locomotive.
  83.  
  84. return - string: The name of the locomotive, with common prefixes and postfixes
  85. removed.
  86. --]]
  87. local function getLocoName(id)
  88.     return string.sub(id, #LOCO_PREFIX + 1, -6)
  89. end
  90.  
  91. local function formatMps(v)
  92.     return string.format("%.2f", v * 3.6)
  93. end
  94.  
  95. --[[
  96. Determines the acceleration needed to go from an initial velocity to a final
  97. velocity in x distance.
  98.  
  99. v_i - float: Initial velocity
  100. v_f - float: Final velocity
  101. x - float: Displacement (distance travelled)
  102.  
  103. return - float: Acceleration needed to achieve the given final velocity in
  104. x distance.
  105. --]]
  106. local function getAcceleration(v_i, v_f, x)
  107.     return ((v_f * v_f) - (v_i * v_i)) / (2 * x)
  108. end
  109.  
  110. --[[
  111. Determines the velocity at which static traction becomes greater than applied
  112. traction. At this point, computations should use the applied traction instead
  113. of static.
  114.  
  115. throttle - float: Throttle setting, between 0 and 1.
  116. horsepower - float: The horsepower of the locomotive.
  117. traction - float: The locomotive's static traction.
  118.  
  119. return - float: The velocity at which the static friction becomes greater
  120. than applied friction, in Km/h
  121. --]]
  122. local function getTractionIntersect(throttle, horsepower, traction)
  123.     local a = 0.004
  124.     local b = -1
  125.     local c = (1855 * throttle^3 * horsepower) / (1.5 * traction * SLIP_CONSTANT)
  126.     local s1 = (-b + math.sqrt(b^2 - 4*a*c)) / (2 * a)
  127.     local s2 = (-b - math.sqrt(b^2 - 4*a*c)) / (2 * a)
  128.     return s2
  129. end
  130.  
  131. local function getBrake(v_i, v_f, x, rolling_resistance_force, traction, total_weight, loco_weight)
  132.    
  133.     target_acceleration = getAcceleration(v_i, v_f, x)
  134.     local num = (-total_weight * target_acceleration - rolling_resistance_force) * (v_f - v_i)
  135.     local C = 20
  136.     local C_1 = 0.39
  137.     local denom = C_1 * (traction + 0.27801375 * (total_weight - loco_weight)) * SLIP_CONSTANT * (((v_f * C) - 0.002 * (v_f * C)^2) - ((v_i * C) - 0.002 * (v_i * C)^2))
  138.  
  139.     return math.sqrt(num / denom)
  140. end
  141.  
  142. --[[
  143. Slows a train down with a constant target acceleration.
  144.  
  145. v_i - float: Initial velocity, in m/s.
  146. v_f - float: Final velocity, in m/s.
  147. x - float: The distance, in meters.
  148. rolling_resistance_force - float: The resistance caused by total weight of the rolling stock.
  149. traction - float: The total static traction of the locomotive. (This is shown in the GUI).
  150. total_weight - float: The total weight of the entire consist.
  151. loco_weight - float: The weight of the locomotive.
  152. --]]
  153. local function slowDown(v_i, v_f, x, rolling_resistance_force, traction, total_weight, loco_weight)
  154.    
  155.     target_acceleration = getAcceleration(v_i, v_f, x)
  156.     local num = (-total_weight * target_acceleration - rolling_resistance_force) * (v_f - v_i)
  157.     local denom = (traction + 0.27801375 * (total_weight - loco_weight)) * SLIP_CONSTANT * (((v_f * 3.6) - 0.002 * (v_f * 3.6)^2) - ((v_i * 3.6) - 0.002 * (v_i * 3.6)^2))
  158.  
  159.     local brake = getBrake(
  160.         v_i,
  161.         v_f,
  162.         x,
  163.         rolling_resistance_force,
  164.         traction,
  165.         total_weight,
  166.         loco_weight
  167.     )
  168.  
  169.     if (VERBOSE) then
  170.         print("  Estimated brake required: " .. brake)
  171.     end
  172.  
  173.     if (not DEBUG) then
  174.         control.setThrottle(0)
  175.         control.setBrake(brake)
  176.         print("[-] " .. formatMps(v_i) .. " Km/h -> " .. formatMps(v_f) .. " Km/h, Brakes set to " .. string.format("%.3f", brake))
  177.         if (brake > 1) then
  178.             print("  ! [WARNING] ! Not enough braking power.")
  179.         end
  180.     end
  181. end
  182.  
  183. local function speedUp(v_i, v_f, target_acceleration, rolling_resistance_force, traction, total_weight, loco_weight, horsepower, half_velocity)
  184.     local required_tractive_effort = target_acceleration * total_weight + rolling_resistance_force
  185.     local integral_force = (v_f - v_i) * required_tractive_effort
  186.  
  187.     if (VERBOSE) then
  188.         print("  Ft = " .. required_tractive_effort .. " N")
  189.         print("  Integral of Ft = " .. integral_force)
  190.         print()
  191.     end
  192.  
  193.     local iteration_count = 0
  194.     local step_size = 0.1
  195.     local t = 0
  196.     -- Repeat until desired precision is reached.
  197.     while (step_size >= THROTTLE_PRECISION) do
  198.         t = t + step_size
  199.  
  200.         local v_half = getTractionIntersect(t, horsepower, traction) / 3.6
  201.  
  202.         -- The static friction can be ignored completely.
  203.  
  204.         local function integrate_applied(t, h, v_i, v_f)
  205.             return 1855 * (t^3) * h * (math.log(v_f) - math.log(v_i))
  206.         end
  207.  
  208.         local function integrate_static(traction, v_i, v_f)
  209.             return 1.5 * traction * SLIP_CONSTANT * ((v_f - 0.002 * (v_f^2)) - (v_i - 0.002 * (v_i^2)))
  210.         end
  211.  
  212.         local integral_static = 1.5 * traction * SLIP_CONSTANT * ((v_half - 0.002 * (v_half^2)) - (v_i - 0.002 * (v_i^2)))
  213.         local integral_applied = 1855 * (t^3) * horsepower * (math.log(v_f) - math.log(v_half))
  214.         local proposed_integral_force = 0
  215.  
  216.         if (v_half <= v_i) then
  217.             -- If the intersection is below the speed range, use only applied.
  218.             proposed_integral_force = integrate_applied(t, horsepower, v_i, v_f)
  219.         elseif (v_half >= v_f) then
  220.             -- If the intersection is above the speed range, use only static.
  221.             proposed_integral_force = integrate_static(t, horsepower, v_i, v_f)
  222.         else
  223.             -- The intersection sits between v_i and v_f, so use both.
  224.             proposed_integral_force = integrate_static(traction, v_i, v_half) + integrate_applied(t, horsepower, v_half, v_f)
  225.         end
  226.  
  227.         if (VERBOSE) then
  228.             print("->| (" .. iteration_count .. ") t = " .. t .. " :")
  229.             print("  | V(1/2) = " .. v_half .. " m/s")
  230.             print("  | Fs + Fa integral = " .. proposed_integral_force)
  231.         end
  232.  
  233.         -- Check if we've overshot the target force, and go back and increase the precision.
  234.         if (proposed_integral_force > integral_force) then
  235.             t = t - step_size
  236.             step_size = step_size / 10
  237.  
  238.             if (VERBOSE) then
  239.                 print("  | Too much! Increasing precision to " .. step_size)
  240.             end
  241.         end
  242.  
  243.         iteration_count = iteration_count + 1
  244.     end
  245.  
  246.     if (VERBOSE) then
  247.         print()
  248.         print("Found target throttle of: " .. t)
  249.         print()
  250.     end
  251.  
  252.     -- Once the throttle has been computed, then actually set the controls on the locomotive.
  253.     if (not DEBUG) then
  254.         control.setBrake(0)
  255.         control.setThrottle(t)
  256.         print("[+] " .. formatMps(v_i) .. " Km/h -> " .. formatMps(v_f) .. " Km/h, Throttle set to " .. t)
  257.         if (t >= 1.0) then
  258.             print("  ! [WARNING] ! Throttle set to MAX, likely insufficient power.")
  259.         end
  260.     end
  261. end
  262.  
  263. --[[
  264. Sets the locomotive to either speed up or slow down to attain a specified final
  265. velocity at a distance of x meters(blocks) away.
  266.  
  267. v_i - float: Initial velocity in Km/h
  268. v_f - float: Final velocity in Km/h
  269. x - float: The distance over which velocity should change, in m.
  270. stock - table: A table of information about the locomotive itself.
  271. consist - table: A table of information about the train as a whole.
  272. --]]
  273. local function setFinalVelocityAtDistance(v_i, v_f, x, stock, consist)
  274.     -- Convert from km/h to m/s
  275.     v_i = v_i / 3.6
  276.     v_f = v_f / 3.6
  277.  
  278.     -- Physics computations.
  279.     local total_weight = consist.weight_kg
  280.     local average_velocity = v_i + (v_f - v_i) / 2
  281.     local target_acceleration = getAcceleration(v_i, v_f, x)
  282.     local time = (v_f - v_i) / target_acceleration
  283.     local half_velocity = math.sqrt(v_i^2 + (2 * target_acceleration * (x / 2)))
  284.     local rolling_resistance_force = total_weight * 0.01471
  285.     local resting_acceleration = -rolling_resistance_force / total_weight
  286.  
  287.     if (VERBOSE) then
  288.         print("[DEBUG-INFO]:")
  289.         print("Vi = " .. v_i .. " m/s")
  290.         print("Vf = " .. v_f .. " m/s")
  291.         print("dX = " .. x .. " m")
  292.         print("Total weight = " .. total_weight .. " Kg")
  293.         print("a = " .. target_acceleration .. " m/s^2, dT = " .. time .. " s")
  294.         print("V(1/2) = " .. half_velocity .. " m/s")
  295.         print("Resting acceleration: " .. resting_acceleration .. " m/s^2")
  296.         print()
  297.     end
  298.  
  299.     -- Locomotive constants.
  300.     loco_name = getLocoName(stock.id)
  301.     weight_loco_kg = stock.weight
  302.     loco_traction = stock.traction
  303.     horsepower = stock.horsepower
  304.  
  305.     print("[i] " .. loco_name .. " passed overhead.")
  306.  
  307.     -- Use the throttle to accelerate.
  308.     if (target_acceleration > resting_acceleration) then
  309.         speedUp(
  310.             v_i,
  311.             v_f,
  312.             target_acceleration,
  313.             rolling_resistance_force,
  314.             loco_traction,
  315.             total_weight,
  316.             weight_loco_kg,
  317.             horsepower,
  318.             half_velocity
  319.         )
  320.     else
  321.         -- Use brakes to slow the train over time.
  322.         slowDown(
  323.             v_i,
  324.             v_f,
  325.             x,
  326.             rolling_resistance_force,
  327.             loco_traction,
  328.             total_weight,
  329.             weight_loco_kg
  330.         )
  331.     end
  332.  
  333.     if (VERBOSE) then
  334.         print("[END]")
  335.         print()
  336.     end
  337. end
  338.  
  339. local function handleEvent(augment_type, stock_uuid, params)
  340.     local stock = detector.info()
  341.     if (augment_type == "LOCO_CONTROL" and stock ~= nil and stock.horsepower ~= nil) then
  342.         -- Assume that the detector and the controller are at the same point.
  343.         local consist = detector.consist()
  344.  
  345.         setFinalVelocityAtDistance(
  346.             consist.speed_km,
  347.             params.final_velocity,
  348.             params.distance,
  349.             stock,
  350.             consist
  351.         )
  352.  
  353.         -- Call user-defined function after all speed-dependent logic is done.
  354.         onTrainOverhead(stock, consist, control)
  355.     end
  356. end
  357.  
  358. --[[
  359. Saves the given table of parameters to a configuration file.
  360.  
  361. params - table: A table of parameters.
  362. --]]
  363. local function saveParameters(params)
  364.     if (fs ~= nil) then
  365.         local f = io.open(CONFIG_FILE, "w")
  366.         f:write(serialization.serialize(params))
  367.         f:close()
  368.     end
  369. end
  370.  
  371. --[[
  372. Reads parameters from a configuration file.
  373.  
  374. return - table: A table of parameters.
  375. --]]
  376. local function readParameters()
  377.     local f = io.open(CONFIG_FILE, "r")
  378.     local params = serialization.unserialize(f:read("*a"))
  379.     f:close()
  380.     return params
  381. end
  382.  
  383. --[[
  384. Using configuration files and command line arguments, get all needed
  385. parameters for the program.
  386.  
  387. args - table: Command line arguments.
  388.  
  389. return - float, float: VF and X
  390. --]]
  391. local function getParameters(args)
  392.     local params = {
  393.         final_velocity = 0,
  394.         distance = 100
  395.     }
  396.    
  397.     -- Attempt to get arguments from command line, if given.
  398.     if (#args == 2) then
  399.         params.final_velocity = tonumber(args[1])
  400.         params.distance = tonumber(args[2])
  401.         saveParameters(params)
  402.         print("[i] Saved config to file.")
  403.     elseif (fs ~= nil) then
  404.         for k,v in pairs(readParameters()) do
  405.             params[k] = v
  406.         end
  407.         print("[i] Loaded config from file.")
  408.     end
  409.  
  410.     return params
  411. end
  412.  
  413. -- DEBUG FUNCTIONS:
  414. local function testRun(v_i, v_f, x, b_e, stock, consist)
  415.     b_a = getBrake(
  416.         v_i / 3.6,
  417.         v_f / 3.6,
  418.         x,
  419.         consist.weight_kg * 0.01471,
  420.         consist.weight_kg,
  421.         stock.traction,
  422.         stock.weight
  423.     )
  424.     local actual = b_a
  425.     local expected = b_e
  426.     local difference = b_a - b_e
  427.     local ratio = b_a / b_e
  428.  
  429.     print("Test Run:")
  430.     print("  v_i      = " .. v_i .. " km/h")
  431.     print("  v_f      = " .. v_f .. " km/h")
  432.     print("  x        = " .. x .. " m")
  433.     print("Results:")
  434.     print("  expected = " .. b_e)
  435.     print("  actual   = " .. b_a)
  436.     print("  diff     = " .. difference)
  437.     print("  ratio    = " .. ratio)
  438.     print()
  439.  
  440. end
  441.  
  442. --------------------------------------------------|
  443. -- ACTUAL SCRIPT: This is the program entry point.|
  444. --------------------------------------------------|
  445.  
  446. if (not DEBUG) then
  447.  
  448.     local params = getParameters({...})
  449.     os.sleep(1)
  450.  
  451.     term.clear()
  452.     print("--------------------------------------")
  453.     print("Immersive Railroading Speed Controller")
  454.     print("--------------------------------------")
  455.     print("| Target Velocity: " .. params.final_velocity .. " Km/h")
  456.     print("| Distance: " .. params.distance .. " m")
  457.     print("--------------------------------------")
  458.  
  459.     while (true) do
  460.         event_name, address, augment_type, stock_uuid = event.pull("ir_train_overhead")
  461.         handleEvent(augment_type, stock_uuid, params)
  462.     end
  463. else
  464.     local stock = {-- Stock
  465.         id = "rolling_stock/locomotives/DEBUG.json",
  466.         weight = 181436,
  467.         traction = 631648,
  468.         horsepower = 4400
  469.     }
  470.     local consist = {-- Consist
  471.         weight_kg = 778204,
  472.         speed_km = 10
  473.     }
  474.     testRun(11.75, 0, 55, 0.054, stock, consist)
  475.     testRun(20.25, 0, 100, 0.123, stock, consist)
  476.     testRun(27.13, 0, 152, 0.143, stock, consist)
  477.     testRun(38.96, 0, 195, 0.211, stock, consist)
  478. end
Add Comment
Please, Sign In to add comment