50_inst_per_sec.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import copy
  4. import math
  5. import os
  6. import sys
  7. import random
  8. from typing import Dict, List, Optional, Tuple
  9. # ====================================
  10. # Constants and Default Values
  11. # ====================================
  12. DEFAULT_BUFFER_FILLING_RATE_IN_C_PER_MS = 50.0 / 1000.0 # The buffer filling rate in #commands/ms
  13. DEFAULT_BUFFER_SIZE = 15 # The buffer size in #commands
  14. ## Gets the code and number from the given g-code line.
  15. def get_code_and_num(gcode_line: str) -> Tuple[str, str]:
  16. gcode_line = gcode_line.strip()
  17. cmd_code = gcode_line[0].upper()
  18. cmd_num = str(gcode_line[1:])
  19. return cmd_code, cmd_num
  20. ## Fetches arguments such as X1 Y2 Z3 from the given part list and returns a
  21. # dict.
  22. def get_value_dict(parts: List[str]) -> Dict[str, str]:
  23. value_dict = {}
  24. for p in parts:
  25. p = p.strip()
  26. if not p:
  27. continue
  28. code, num = get_code_and_num(p)
  29. value_dict[code] = num
  30. return value_dict
  31. # ============================
  32. # Math Functions - Begin
  33. # ============================
  34. def calc_distance(pos1, pos2):
  35. delta = {k: pos1[k] - pos2[k] for k in pos1}
  36. distance = 0
  37. for value in delta.values():
  38. distance += value ** 2
  39. distance = math.sqrt(distance)
  40. return distance
  41. ## Given the initial speed, the target speed, and the acceleration, calculate
  42. # the distance that's neede for the acceleration to finish.
  43. def calc_acceleration_distance(init_speed: float, target_speed: float, acceleration: float) -> float:
  44. if acceleration == 0:
  45. return 0.0
  46. return (target_speed ** 2 - init_speed ** 2) / (2 * acceleration)
  47. def calc_travel_time(p0, p1, init_speed: float, target_speed: float, acceleration: float) -> float:
  48. pass
  49. ## Calculates the point at which you must start braking.
  50. #
  51. # This gives the distance from the start of a line at which you must start
  52. # decelerating (at a rate of `-acceleration`) if you started at speed
  53. # `initial_feedrate` and accelerated until this point and want to end at the
  54. # `final_feedrate` after a total travel of `distance`. This can be used to
  55. # compute the intersection point between acceleration and deceleration in the
  56. # cases where the trapezoid has no plateau (i.e. never reaches maximum speed).
  57. def calc_intersection_distance(initial_feedrate: float, final_feedrate: float, acceleration: float, distance: float) -> float:
  58. if acceleration == 0:
  59. return 0
  60. return (2 * acceleration * distance - initial_feedrate * initial_feedrate + final_feedrate * final_feedrate) / (4 * acceleration)
  61. class State:
  62. def __init__(self, previous_state: Optional["State"]) -> None:
  63. self.X = 0.0
  64. self.Y = 0.0
  65. self.Z = 0.0
  66. self.E = 0.0
  67. self.F = 0.0
  68. self.speed = {"X": 0.0,
  69. "Y": 0.0,
  70. "Z": 0.0,
  71. }
  72. self.accelerations = {"XY": 0.0,
  73. "Z": 0.0,
  74. "S": 0.0, # printing
  75. "T": 0.0, # travel
  76. }
  77. self.jerks = {"X": 0.0,
  78. "Y": 0.0,
  79. "Z": 0.0,
  80. }
  81. self.in_relative_positioning_mode = False # type: bool
  82. self.in_relative_extrusion_mode = False # type: bool
  83. if previous_state is not None:
  84. self.X = previous_state.X
  85. self.Y = previous_state.Y
  86. self.Z = previous_state.Z
  87. self.E = previous_state.E
  88. self.F = previous_state.F
  89. self.speed = copy.deepcopy(previous_state.speed)
  90. self.accelerations = copy.deepcopy(previous_state.accelerations)
  91. self.jerks = copy.deepcopy(previous_state.jerks)
  92. self.in_relative_positioning_mode = previous_state.in_relative_positioning_mode
  93. self.in_relative_extrusion_mode = previous_state.in_relative_extrusion_mode
  94. class Command:
  95. def __init__(self, cmd_str: str, previous_state: "State") -> None:
  96. self._cmd_str = cmd_str # type: str
  97. self._previous_state = previous_state # type: State
  98. self._after_state = State(previous_state) # type: State
  99. self._distance_in_mm = 0.0 # type float
  100. self._estimated_exec_time_in_ms = 0.0 # type: float
  101. self._cmd_process_function_map = {
  102. "G": self._handle_g,
  103. "M": self._handle_m,
  104. "T": self._handle_t,
  105. }
  106. self._is_comment = False # type: bool
  107. self._is_empty = False # type: bool
  108. #Fields taken from CuraEngine's implementation.
  109. self._recalculate = False
  110. self._accelerate_until = 0
  111. self._decelerate_after = 0
  112. self._initial_feedrate = 0
  113. self._final_feedrate = 0
  114. self._entry_speed = 0
  115. self._max_entry_speed =0
  116. self._nominal_length = False
  117. self._nominal_feedrate = 0
  118. self._max_travel = 0
  119. self._distance = 0
  120. self._acceleration = 0
  121. self._delta = [0, 0, 0]
  122. self._abs_delta = [0, 0, 0]
  123. ## Calculate the velocity-time trapezoid function for this move.
  124. #
  125. # Each move has a three-part function mapping time to velocity.
  126. def calculate_trapezoid(self, entry_factor, exit_factor):
  127. initial_feedrate = self._nominal_feedrate * entry_factor
  128. final_feedrate = self._nominal_feedrate * exit_factor
  129. #How far are we accelerating and how far are we decelerating?
  130. accelerate_distance = calc_acceleration_distance(initial_feedrate, self._nominal_feedrate, self._acceleration)
  131. decelerate_distance = calc_acceleration_distance(self._nominal_feedrate, final_feedrate, -self._acceleration)
  132. plateau_distance = self._distance - accelerate_distance - decelerate_distance #And how far in between at max speed?
  133. #Is the plateau negative size? That means no cruising, and we'll have to
  134. #use intersection_distance to calculate when to abort acceleration and
  135. #start braking in order to reach the final_rate exactly at the end of
  136. #this command.
  137. if plateau_distance < 0:
  138. accelerate_distance = calc_intersection_distance(initial_feedrate, final_feedrate, self._acceleration, self._distance)
  139. accelerate_distance = max(accelerate_distance, 0) #Due to rounding errors.
  140. accelerate_distance = min(accelerate_distance, self._distance)
  141. plateau_distance = 0
  142. self._accelerate_until = accelerate_distance
  143. self._decelerate_after = accelerate_distance + plateau_distance
  144. self._initial_feedrate = initial_feedrate
  145. self._final_feedrate = final_feedrate
  146. def get_after_state(self) -> State:
  147. return self._after_state
  148. @property
  149. def is_command(self) -> bool:
  150. return not self._is_comment and not self._is_empty
  151. @property
  152. def estimated_exec_time_in_ms(self) -> float:
  153. return self._estimated_exec_time_in_ms
  154. def __str__(self) -> str:
  155. if self._is_comment or self._is_empty:
  156. return self._cmd_str
  157. distance_in_mm = round(self._distance_in_mm, 5)
  158. info = "d=%s f=%s t=%s" % (distance_in_mm, self._after_state.F, self._estimated_exec_time_in_ms)
  159. return self._cmd_str.strip() + " ; --- " + info + os.linesep
  160. ## Estimates the execution time of this command and calculates the state
  161. # after this command is executed.
  162. def process(self) -> None:
  163. line = self._cmd_str.strip()
  164. if not line:
  165. self._is_empty = True
  166. return
  167. if line.startswith(";"):
  168. self._is_comment = True
  169. return
  170. # Remove comment
  171. line = line.split(";", 1)[0].strip()
  172. parts = line.split(" ")
  173. cmd_code, cmd_num = get_code_and_num(parts[0])
  174. cmd_num = int(cmd_num)
  175. func = self._cmd_process_function_map.get(cmd_code)
  176. if func is None:
  177. print("!!! no handle function for command type [%s]" % cmd_code)
  178. return
  179. func(cmd_num, parts)
  180. def _handle_g(self, cmd_num: int, parts: List[str]) -> None:
  181. estimated_exec_time_in_ms = 0.0
  182. # G0 and G1: Move
  183. if cmd_num in (0, 1):
  184. # Move
  185. distance = 0.0
  186. if len(parts) > 0:
  187. value_dict = get_value_dict(parts[1:])
  188. for key, value in value_dict.items():
  189. setattr(self._after_state, key, float(value))
  190. current_position = {"X": self._previous_state.X,
  191. "Y": self._previous_state.Y,
  192. "Z": self._previous_state.Z,
  193. }
  194. new_position = copy.deepcopy(current_position)
  195. for key in new_position:
  196. new_value = float(value_dict.get(key, new_position[key]))
  197. new_position[key] = new_value
  198. distance = calc_distance(current_position, new_position)
  199. self._distance_in_mm = distance
  200. travel_time_in_ms = distance / (self._after_state.F / 60.0) * 1000.0
  201. estimated_exec_time_in_ms = travel_time_in_ms
  202. # TODO: take acceleration into account
  203. # G4: Dwell, pause the machine for a period of time. TODO
  204. if cmd_num == 4:
  205. # Pnnn is time to wait in milliseconds (P0 wait until all previous moves are finished)
  206. cmd, num = get_code_and_num(parts[1])
  207. num = float(num)
  208. if cmd == "P":
  209. if num > 0:
  210. estimated_exec_time_in_ms = num
  211. # G10: Retract. Assume 0.3 seconds for short retractions and 0.5 seconds for long retractions.
  212. if cmd_num == 10:
  213. # S0 is short retract (default), S1 is long retract
  214. is_short_retract = True
  215. if len(parts) > 1:
  216. cmd, num = get_code_and_num(parts[1])
  217. if cmd == "S" and num == 1:
  218. is_short_retract = False
  219. estimated_exec_time_in_ms = (0.3 if is_short_retract else 0.5) * 1000
  220. # G11: Unretract. Assume 0.5 seconds.
  221. if cmd_num == 11:
  222. estimated_exec_time_in_ms = 0.5 * 1000
  223. # G90: Set to absolute positioning. Assume 0 seconds.
  224. if cmd_num == 90:
  225. self._after_state.in_relative_positioning_mode = False
  226. estimated_exec_time_in_ms = 0.0
  227. # G91: Set to relative positioning. Assume 0 seconds.
  228. if cmd_num == 91:
  229. self._after_state.in_relative_positioning_mode = True
  230. estimated_exec_time_in_ms = 0.0
  231. # G92: Set position. Assume 0 seconds.
  232. if cmd_num == 92:
  233. # TODO: check
  234. value_dict = get_value_dict(parts[1:])
  235. for key, value in value_dict.items():
  236. setattr(self._previous_state, key, value)
  237. # G280: Prime. Assume 10 seconds for using blob and 5 seconds for no blob.
  238. if cmd_num == 280:
  239. use_blob = True
  240. if len(parts) > 1:
  241. cmd, num = get_code_and_num(parts[1])
  242. if cmd == "S" and num == 1:
  243. use_blob = False
  244. estimated_exec_time_in_ms = (10.0 if use_blob else 5.0) * 1000
  245. # Update estimated execution time
  246. self._estimated_exec_time_in_ms = round(estimated_exec_time_in_ms, 5)
  247. def _handle_m(self, cmd_num: int, parts: List[str]) -> None:
  248. estimated_exec_time_in_ms = 0.0
  249. # M82: Set extruder to absolute mode. Assume 0 execution time.
  250. if cmd_num == 82:
  251. self._after_state.in_relative_extrusion_mode = False
  252. estimated_exec_time_in_ms = 0.0
  253. # M83: Set extruder to relative mode. Assume 0 execution time.
  254. if cmd_num == 83:
  255. self._after_state.in_relative_extrusion_mode = True
  256. estimated_exec_time_in_ms = 0.0
  257. # M104: Set extruder temperature (no wait). Assume 0 execution time.
  258. if cmd_num == 104:
  259. estimated_exec_time_in_ms = 0.0
  260. # M106: Set fan speed. Assume 0 execution time.
  261. if cmd_num == 106:
  262. estimated_exec_time_in_ms = 0.0
  263. # M107: Turn fan off. Assume 0 execution time.
  264. if cmd_num == 107:
  265. estimated_exec_time_in_ms = 0.0
  266. # M109: Set extruder temperature (wait). Uniformly random time between 30 - 90 seconds.
  267. if cmd_num == 109:
  268. estimated_exec_time_in_ms = random.uniform(30, 90) * 1000 # TODO: Check
  269. # M140: Set bed temperature (no wait). Assume 0 execution time.
  270. if cmd_num == 140:
  271. estimated_exec_time_in_ms = 0.0
  272. # M204: Set default acceleration. Assume 0 execution time.
  273. if cmd_num == 204:
  274. value_dict = get_value_dict(parts[1:])
  275. for key, value in value_dict.items():
  276. self._after_state.accelerations[key] = float(value)
  277. estimated_exec_time_in_ms = 0.0
  278. # M205: Advanced settings, we only set jerks for Griffin. Assume 0 execution time.
  279. if cmd_num == 205:
  280. value_dict = get_value_dict(parts[1:])
  281. for key, value in value_dict.items():
  282. self._after_state.jerks[key] = float(value)
  283. estimated_exec_time_in_ms = 0.0
  284. self._estimated_exec_time_in_ms = estimated_exec_time_in_ms
  285. def _handle_t(self, cmd_num: int, parts: List[str]) -> None:
  286. # Tn: Switching extruder. Assume 2 seconds.
  287. estimated_exec_time_in_ms = 2.0
  288. self._estimated_exec_time_in_ms = estimated_exec_time_in_ms
  289. class CommandBuffer:
  290. def __init__(self, all_lines: List[str],
  291. buffer_filling_rate: float = DEFAULT_BUFFER_FILLING_RATE_IN_C_PER_MS,
  292. buffer_size: int = DEFAULT_BUFFER_SIZE
  293. ) -> None:
  294. self._all_lines = all_lines
  295. self._all_commands = list()
  296. self._buffer_filling_rate = buffer_filling_rate # type: float
  297. self._buffer_size = buffer_size # type: int
  298. # If the buffer can depletes less than this amount time, it can be filled up in time.
  299. lower_bound_buffer_depletion_time = self._buffer_size / self._buffer_filling_rate # type: float
  300. self._detection_time_frame = lower_bound_buffer_depletion_time
  301. self._code_count_limit = self._buffer_size
  302. print("Time Frame: %s" % self._detection_time_frame)
  303. print("Code Limit: %s" % self._code_count_limit)
  304. self._bad_frame_ranges = []
  305. def process(self) -> None:
  306. previous_state = None
  307. cmd0_idx = 0
  308. total_frame_time_in_ms = 0.0
  309. cmd_count = 0
  310. for idx, line in enumerate(self._all_lines):
  311. cmd = Command(line, previous_state)
  312. cmd.process()
  313. self._all_commands.append(cmd)
  314. previous_state = cmd.get_after_state()
  315. if not cmd.is_command:
  316. continue
  317. cmd_count += 1
  318. if idx > cmd0_idx or idx == 0:
  319. total_frame_time_in_ms += cmd.estimated_exec_time_in_ms
  320. if total_frame_time_in_ms > 1000.0:
  321. # Find the next starting command which makes the total execution time of the frame to be less than
  322. # 1 second.
  323. cmd0_idx += 1
  324. total_frame_time_in_ms -= self._all_commands[cmd0_idx].estimated_exec_time_in_ms
  325. cmd_count -= 1
  326. while total_frame_time_in_ms > 1000.0:
  327. cmd0_idx += 1
  328. total_frame_time_in_ms -= self._all_commands[cmd0_idx].estimated_exec_time_in_ms
  329. cmd_count -= 1
  330. # If within the current time frame the code count exceeds the limit, record that.
  331. if total_frame_time_in_ms <= self._detection_time_frame and cmd_count > self._code_count_limit:
  332. need_to_append = True
  333. if self._bad_frame_ranges:
  334. last_item = self._bad_frame_ranges[-1]
  335. if last_item["start_line"] == cmd0_idx:
  336. last_item["end_line"] = idx
  337. last_item["cmd_count"] = cmd_count
  338. last_item["time_in_ms"] = total_frame_time_in_ms
  339. need_to_append = False
  340. if need_to_append:
  341. self._bad_frame_ranges.append({"start_line": cmd0_idx,
  342. "end_line": idx,
  343. "cmd_count": cmd_count,
  344. "time_in_ms": total_frame_time_in_ms})
  345. def to_file(self, file_name: str) -> None:
  346. all_lines = [str(c) for c in self._all_commands]
  347. with open(file_name, "w", encoding = "utf-8") as f:
  348. f.writelines(all_lines)
  349. def report(self) -> None:
  350. for item in self._bad_frame_ranges:
  351. print("!!!!! potential bad frame from line %s to %s, code count = %s, in %s ms" % (
  352. item["start_line"], item["end_line"], item["cmd_count"], round(item["time_in_ms"], 4)))
  353. if __name__ == "__main__":
  354. if len(sys.argv) != 3:
  355. print("Usage: <input gcode> <output gcode>")
  356. sys.exit(1)
  357. in_filename = sys.argv[1]
  358. out_filename = sys.argv[2]
  359. with open(in_filename, "r", encoding = "utf-8") as f:
  360. all_lines = f.readlines()
  361. buf = CommandBuffer(all_lines)
  362. buf.process()
  363. buf.to_file(out_filename)
  364. buf.report()